Compare commits

...

10 Commits

Author SHA1 Message Date
Moody Salem
f289dec684 fix(multicall reducer): add test and fix update multicall results 2020-05-30 03:10:49 -04:00
Moody Salem
73d3df05f2 perf(network connector): use an inline network connector until improved connector is pushed upstream 2020-05-30 02:03:02 -04:00
Moody Salem
83554f44f8 perf(multicall): add unit tests and fix a bug (#845)
* start with the migrate page

* Add a bunch of tests and bump up the call size

* Show a link to the old portal, disable the WIP page

* Fix lint error
2020-05-29 20:07:18 -04:00
Moody Salem
320b2e384b chore(constant): update token list 2020-05-29 15:20:23 -04:00
Moody Salem
9492e7375a chore(multicall/migrate): move some v1 stuff around for migrate 2020-05-29 15:08:07 -04:00
Moody Salem
8a6a10be9d chore(multicall): lint error 2020-05-29 13:25:55 -04:00
Moody Salem
5e486fca7f perf(multicall): improve fetching code to allow for fetching immutable data like token symbols/names/decimals 2020-05-29 13:23:49 -04:00
Moody Salem
87d24c404b fix(multicall): v1 pair lookup 2020-05-29 11:52:20 -04:00
Moody Salem
d4011f73d1 fix(multicall): return loading states from the multicall hooks #842 2020-05-29 11:48:33 -04:00
Moody Salem
6fc3157977 chore(strict): strict connectors directory 2020-05-29 11:05:54 -04:00
29 changed files with 1995 additions and 3226 deletions

View File

@@ -38,7 +38,6 @@
"@web3-react/core": "^6.0.9",
"@web3-react/fortmatic-connector": "^6.0.9",
"@web3-react/injected-connector": "^6.0.7",
"@web3-react/network-connector": "^6.0.9",
"@web3-react/portis-connector": "^6.0.9",
"@web3-react/walletconnect-connector": "^6.0.9",
"@web3-react/walletlink-connector": "^6.0.9",
@@ -50,7 +49,6 @@
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^4.0.0",
"history": "^4.9.0",
"i18next": "^15.0.9",
"i18next-browser-languagedetector": "^3.0.1",
"i18next-xhr-backend": "^2.0.1",

View File

@@ -98,7 +98,7 @@ function PopupItem({ content, popKey }: { content: PopupContent; popKey: string
}
}
export default function App() {
export default function Popups() {
const theme = useContext(ThemeContext)
// get all popups
const activePopups = useActivePopups()

View File

@@ -45,20 +45,6 @@ export default function Web3ReactManager({ children }) {
}
}, [triedEager, networkActive, networkError, activateNetwork, active])
// 'pause' the network connector if we're ever connected to an account and it's active
useEffect(() => {
if (active && networkActive) {
network.pause()
}
}, [active, networkActive])
// 'resume' the network connector if we're ever not connected to an account and it's active
useEffect(() => {
if (!active && networkActive) {
network.resume()
}
}, [active, networkActive])
// when there's no account connected, react to logins (broadly speaking) on the injected provider, if it exists
useInactiveListener(!triedEager)

View File

@@ -1,12 +1,15 @@
import { ChainId } from '@uniswap/sdk'
import { FortmaticConnector as FortmaticConnectorCore } from '@web3-react/fortmatic-connector'
export const OVERLAY_READY = 'OVERLAY_READY'
const chainIdToNetwork = {
1: 'mainnet',
3: 'ropsten',
4: 'rinkeby',
42: 'kovan'
type FormaticSupportedChains = Extract<ChainId, ChainId.MAINNET | ChainId.ROPSTEN | ChainId.RINKEBY | ChainId.KOVAN>
const CHAIN_ID_NETWORK_ARGUMENT: { readonly [chainId in FormaticSupportedChains]: string | undefined } = {
[ChainId.MAINNET]: undefined,
[ChainId.ROPSTEN]: 'ropsten',
[ChainId.RINKEBY]: 'rinkeby',
[ChainId.KOVAN]: 'kovan'
}
export class FortmaticConnector extends FortmaticConnectorCore {
@@ -14,7 +17,11 @@ export class FortmaticConnector extends FortmaticConnectorCore {
if (!this.fortmatic) {
const { default: Fortmatic } = await import('fortmatic')
const { apiKey, chainId } = this as any
this.fortmatic = new Fortmatic(apiKey, chainId === 1 || chainId === 4 ? undefined : chainIdToNetwork[chainId])
if (chainId in CHAIN_ID_NETWORK_ARGUMENT) {
this.fortmatic = new Fortmatic(apiKey, CHAIN_ID_NETWORK_ARGUMENT[chainId as FormaticSupportedChains])
} else {
throw new Error(`Unsupported network ID: ${chainId}`)
}
}
const provider = this.fortmatic.getProvider()
@@ -29,7 +36,10 @@ export class FortmaticConnector extends FortmaticConnectorCore {
}, 200)
})
const [account] = await Promise.all([provider.enable().then(accounts => accounts[0]), pollForOverlayReady])
const [account] = await Promise.all([
provider.enable().then((accounts: string[]) => accounts[0]),
pollForOverlayReady
])
return { provider: this.fortmatic.getProvider(), chainId: (this as any).chainId, account }
}

View File

@@ -1,15 +0,0 @@
import { NetworkConnector as NetworkConnectorCore } from '@web3-react/network-connector'
export class NetworkConnector extends NetworkConnectorCore {
pause() {
if ((this as any).active) {
;(this as any).providers[(this as any).currentChainId].stop()
}
}
resume() {
if ((this as any).active) {
;(this as any).providers[(this as any).currentChainId].start()
}
}
}

View File

@@ -0,0 +1,105 @@
import { ConnectorUpdate } from '@web3-react/types'
import { AbstractConnector } from '@web3-react/abstract-connector'
import invariant from 'tiny-invariant'
interface NetworkConnectorArguments {
urls: { [chainId: number]: string }
defaultChainId?: number
}
// taken from ethers.js, compatible interface with web3 provider
type AsyncSendable = {
isMetaMask?: boolean
host?: string
path?: string
sendAsync?: (request: any, callback: (error: any, response: any) => void) => void
send?: (request: any, callback: (error: any, response: any) => void) => void
}
class RequestError extends Error {
constructor(message: string, public code: number, public data?: unknown) {
super(message)
}
}
class MiniRpcProvider implements AsyncSendable {
public readonly isMetaMask: false = false
public readonly chainId: number
public readonly url: string
public readonly host: string
public readonly path: string
constructor(chainId: number, url: string) {
this.chainId = chainId
this.url = url
const parsed = new URL(url)
this.host = parsed.host
this.path = parsed.pathname
}
public readonly sendAsync = (
request: { jsonrpc: '2.0'; id: number | string | null; method: string; params?: unknown[] | object },
callback: (error: any, response: any) => void
): void => {
this.request(request.method, request.params)
.then(result => callback(null, { jsonrpc: '2.0', id: request.id, result }))
.catch(error => callback(error, null))
}
public readonly request = async (method: string, params?: unknown[] | object): Promise<unknown> => {
const response = await fetch(this.url, {
method: 'POST',
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method,
params
})
})
if (!response.ok) throw new RequestError(`${response.status}: ${response.statusText}`, -32000)
const body = await response.json()
if ('error' in body) {
throw new RequestError(body?.error?.message, body?.error?.code, body?.error?.data)
} else if ('result' in body) {
return body.result
} else {
throw new RequestError(`Received unexpected JSON-RPC response to ${method} request.`, -32000, body)
}
}
}
export class NetworkConnector extends AbstractConnector {
private readonly providers: { [chainId: number]: MiniRpcProvider }
private currentChainId: number
constructor({ urls, defaultChainId }: NetworkConnectorArguments) {
invariant(defaultChainId || Object.keys(urls).length === 1, 'defaultChainId is a required argument with >1 url')
super({ supportedChainIds: Object.keys(urls).map((k): number => Number(k)) })
this.currentChainId = defaultChainId || Number(Object.keys(urls)[0])
this.providers = Object.keys(urls).reduce<{ [chainId: number]: MiniRpcProvider }>((accumulator, chainId) => {
accumulator[Number(chainId)] = new MiniRpcProvider(Number(chainId), urls[Number(chainId)])
return accumulator
}, {})
}
public async activate(): Promise<ConnectorUpdate> {
return { provider: this.providers[this.currentChainId], chainId: this.currentChainId, account: null }
}
public async getProvider(): Promise<MiniRpcProvider> {
return this.providers[this.currentChainId]
}
public async getChainId(): Promise<number> {
return this.currentChainId
}
public async getAccount(): Promise<null> {
return null
}
public deactivate() {
return
}
}

1
src/connectors/fortmatic.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module 'formatic'

View File

@@ -3,15 +3,20 @@ import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
import { WalletLinkConnector } from '@web3-react/walletlink-connector'
import { PortisConnector } from '@web3-react/portis-connector'
import { NetworkConnector } from './Network'
import { FortmaticConnector } from './Fortmatic'
import { NetworkConnector } from './NetworkConnector'
const POLLING_INTERVAL = 10000
const NETWORK_URL = process.env.REACT_APP_NETWORK_URL
const FORMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY
const PORTIS_ID = process.env.REACT_APP_PORTIS_ID
if (typeof NETWORK_URL === 'undefined') {
throw new Error(`REACT_APP_NETWORK_URL must be a defined environment variable`)
}
export const network = new NetworkConnector({
urls: { [Number(process.env.REACT_APP_CHAIN_ID)]: NETWORK_URL },
pollingInterval: POLLING_INTERVAL * 3
urls: { [Number(process.env.REACT_APP_CHAIN_ID)]: NETWORK_URL }
})
export const injected = new InjectedConnector({
@@ -28,13 +33,13 @@ export const walletconnect = new WalletConnectConnector({
// mainnet only
export const fortmatic = new FortmaticConnector({
apiKey: process.env.REACT_APP_FORTMATIC_KEY,
apiKey: FORMATIC_KEY ?? '',
chainId: 1
})
// mainnet only
export const portis = new PortisConnector({
dAppId: process.env.REACT_APP_PORTIS_ID,
dAppId: PORTIS_ID ?? '',
networks: [1]
})

View File

@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.strict.json",
"include": ["**/*"]
}

View File

@@ -1,8 +1,6 @@
import { ChainId, Token, WETH, JSBI, Percent } from '@uniswap/sdk'
import { ChainId, JSBI, Percent, Token, WETH } from '@uniswap/sdk'
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
export const V1_FACTORY_ADDRESS = '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
export const ROUTER_ADDRESS = '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a'
// used for display in the default list when adding liquidity

View File

@@ -15,6 +15,7 @@ export default [
new Token(ChainId.MAINNET, '0x4F9254C83EB525f9FCf346490bbb3ed28a81C667', 18, 'CELR', 'CelerToken'),
new Token(ChainId.MAINNET, '0xF5DCe57282A584D2746FaF1593d3121Fcac444dC', 8, 'cSAI', 'Compound Dai'),
new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
new Token(ChainId.MAINNET, '0xaaAEBE6Fe48E54f431b0C390CfaF0b017d09D42d', 4, 'CEL', 'Celsius'),
new Token(ChainId.MAINNET, '0x06AF07097C9Eeb7fD685c692751D5C66dB49c215', 18, 'CHAI', 'Chai'),
new Token(ChainId.MAINNET, '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', 18, 'SAI', 'Dai Stablecoin v1.0 (SAI)'),
@@ -90,6 +91,7 @@ export default [
new Token(ChainId.MAINNET, '0x42d6622deCe394b54999Fbd73D108123806f6a18', 18, 'SPANK', 'SPANK'),
new Token(ChainId.MAINNET, '0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC', 8, 'STORJ', 'StorjToken'),
new Token(ChainId.MAINNET, '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51', 18, 'sUSD', 'Synth sUSD'),
new Token(ChainId.MAINNET, '0x261EfCdD24CeA98652B9700800a13DfBca4103fF', 18, 'sXAU', 'Synth sXAU'),
new Token(ChainId.MAINNET, '0x8CE9137d39326AD0cD6491fb5CC0CbA0e089b6A9', 18, 'SXP', 'Swipe'),
new Token(ChainId.MAINNET, '0x00006100F7090010005F1bd7aE6122c3C2CF0090', 18, 'TAUD', 'TrueAUD'),
new Token(ChainId.MAINNET, '0x00000100F2A2bd000715001920eB70D229700085', 18, 'TCAD', 'TrueCAD'),

View File

@@ -0,0 +1,9 @@
import { Interface } from '@ethersproject/abi'
import V1_EXCHANGE_ABI from './v1_exchange.json'
import V1_FACTORY_ABI from './v1_factory.json'
const V1_FACTORY_ADDRESS = '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
const V1_FACTORY_INTERFACE = new Interface(V1_FACTORY_ABI)
const V1_EXCHANGE_INTERFACE = new Interface(V1_EXCHANGE_ABI)
export { V1_FACTORY_ADDRESS, V1_FACTORY_INTERFACE, V1_FACTORY_ABI, V1_EXCHANGE_INTERFACE, V1_EXCHANGE_ABI }

View File

@@ -0,0 +1,971 @@
[
{
"name": "TokenPurchase",
"inputs": [
{
"type": "address",
"name": "buyer",
"indexed": true
},
{
"type": "uint256",
"name": "eth_sold",
"indexed": true
},
{
"type": "uint256",
"name": "tokens_bought",
"indexed": true
}
],
"anonymous": false,
"type": "event"
},
{
"name": "EthPurchase",
"inputs": [
{
"type": "address",
"name": "buyer",
"indexed": true
},
{
"type": "uint256",
"name": "tokens_sold",
"indexed": true
},
{
"type": "uint256",
"name": "eth_bought",
"indexed": true
}
],
"anonymous": false,
"type": "event"
},
{
"name": "AddLiquidity",
"inputs": [
{
"type": "address",
"name": "provider",
"indexed": true
},
{
"type": "uint256",
"name": "eth_amount",
"indexed": true
},
{
"type": "uint256",
"name": "token_amount",
"indexed": true
}
],
"anonymous": false,
"type": "event"
},
{
"name": "RemoveLiquidity",
"inputs": [
{
"type": "address",
"name": "provider",
"indexed": true
},
{
"type": "uint256",
"name": "eth_amount",
"indexed": true
},
{
"type": "uint256",
"name": "token_amount",
"indexed": true
}
],
"anonymous": false,
"type": "event"
},
{
"name": "Transfer",
"inputs": [
{
"type": "address",
"name": "_from",
"indexed": true
},
{
"type": "address",
"name": "_to",
"indexed": true
},
{
"type": "uint256",
"name": "_value",
"indexed": false
}
],
"anonymous": false,
"type": "event"
},
{
"name": "Approval",
"inputs": [
{
"type": "address",
"name": "_owner",
"indexed": true
},
{
"type": "address",
"name": "_spender",
"indexed": true
},
{
"type": "uint256",
"name": "_value",
"indexed": false
}
],
"anonymous": false,
"type": "event"
},
{
"name": "setup",
"outputs": [],
"inputs": [
{
"type": "address",
"name": "token_addr"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "addLiquidity",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "min_liquidity"
},
{
"type": "uint256",
"name": "max_tokens"
},
{
"type": "uint256",
"name": "deadline"
}
],
"constant": false,
"payable": true,
"type": "function"
},
{
"name": "removeLiquidity",
"outputs": [
{
"type": "uint256",
"name": "outA"
},
{
"type": "uint256",
"name": "outB"
}
],
"inputs": [
{
"type": "uint256",
"name": "amount"
},
{
"type": "uint256",
"name": "min_eth"
},
{
"type": "uint256",
"name": "min_tokens"
},
{
"type": "uint256",
"name": "deadline"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "__default__",
"outputs": [],
"inputs": [],
"constant": false,
"payable": true,
"type": "function"
},
{
"name": "ethToTokenSwapInput",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "min_tokens"
},
{
"type": "uint256",
"name": "deadline"
}
],
"constant": false,
"payable": true,
"type": "function"
},
{
"name": "ethToTokenTransferInput",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "min_tokens"
},
{
"type": "uint256",
"name": "deadline"
},
{
"type": "address",
"name": "recipient"
}
],
"constant": false,
"payable": true,
"type": "function"
},
{
"name": "ethToTokenSwapOutput",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "tokens_bought"
},
{
"type": "uint256",
"name": "deadline"
}
],
"constant": false,
"payable": true,
"type": "function"
},
{
"name": "ethToTokenTransferOutput",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "tokens_bought"
},
{
"type": "uint256",
"name": "deadline"
},
{
"type": "address",
"name": "recipient"
}
],
"constant": false,
"payable": true,
"type": "function"
},
{
"name": "tokenToEthSwapInput",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "tokens_sold"
},
{
"type": "uint256",
"name": "min_eth"
},
{
"type": "uint256",
"name": "deadline"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "tokenToEthTransferInput",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "tokens_sold"
},
{
"type": "uint256",
"name": "min_eth"
},
{
"type": "uint256",
"name": "deadline"
},
{
"type": "address",
"name": "recipient"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "tokenToEthSwapOutput",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "eth_bought"
},
{
"type": "uint256",
"name": "max_tokens"
},
{
"type": "uint256",
"name": "deadline"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "tokenToEthTransferOutput",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "eth_bought"
},
{
"type": "uint256",
"name": "max_tokens"
},
{
"type": "uint256",
"name": "deadline"
},
{
"type": "address",
"name": "recipient"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "tokenToTokenSwapInput",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "tokens_sold"
},
{
"type": "uint256",
"name": "min_tokens_bought"
},
{
"type": "uint256",
"name": "min_eth_bought"
},
{
"type": "uint256",
"name": "deadline"
},
{
"type": "address",
"name": "token_addr"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "tokenToTokenTransferInput",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "tokens_sold"
},
{
"type": "uint256",
"name": "min_tokens_bought"
},
{
"type": "uint256",
"name": "min_eth_bought"
},
{
"type": "uint256",
"name": "deadline"
},
{
"type": "address",
"name": "recipient"
},
{
"type": "address",
"name": "token_addr"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "tokenToTokenSwapOutput",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "tokens_bought"
},
{
"type": "uint256",
"name": "max_tokens_sold"
},
{
"type": "uint256",
"name": "max_eth_sold"
},
{
"type": "uint256",
"name": "deadline"
},
{
"type": "address",
"name": "token_addr"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "tokenToTokenTransferOutput",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "tokens_bought"
},
{
"type": "uint256",
"name": "max_tokens_sold"
},
{
"type": "uint256",
"name": "max_eth_sold"
},
{
"type": "uint256",
"name": "deadline"
},
{
"type": "address",
"name": "recipient"
},
{
"type": "address",
"name": "token_addr"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "tokenToExchangeSwapInput",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "tokens_sold"
},
{
"type": "uint256",
"name": "min_tokens_bought"
},
{
"type": "uint256",
"name": "min_eth_bought"
},
{
"type": "uint256",
"name": "deadline"
},
{
"type": "address",
"name": "exchange_addr"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "tokenToExchangeTransferInput",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "tokens_sold"
},
{
"type": "uint256",
"name": "min_tokens_bought"
},
{
"type": "uint256",
"name": "min_eth_bought"
},
{
"type": "uint256",
"name": "deadline"
},
{
"type": "address",
"name": "recipient"
},
{
"type": "address",
"name": "exchange_addr"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "tokenToExchangeSwapOutput",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "tokens_bought"
},
{
"type": "uint256",
"name": "max_tokens_sold"
},
{
"type": "uint256",
"name": "max_eth_sold"
},
{
"type": "uint256",
"name": "deadline"
},
{
"type": "address",
"name": "exchange_addr"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "tokenToExchangeTransferOutput",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "tokens_bought"
},
{
"type": "uint256",
"name": "max_tokens_sold"
},
{
"type": "uint256",
"name": "max_eth_sold"
},
{
"type": "uint256",
"name": "deadline"
},
{
"type": "address",
"name": "recipient"
},
{
"type": "address",
"name": "exchange_addr"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "getEthToTokenInputPrice",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "eth_sold"
}
],
"constant": true,
"payable": false,
"type": "function"
},
{
"name": "getEthToTokenOutputPrice",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "tokens_bought"
}
],
"constant": true,
"payable": false,
"type": "function"
},
{
"name": "getTokenToEthInputPrice",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "tokens_sold"
}
],
"constant": true,
"payable": false,
"type": "function"
},
{
"name": "getTokenToEthOutputPrice",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "eth_bought"
}
],
"constant": true,
"payable": false,
"type": "function"
},
{
"name": "tokenAddress",
"outputs": [
{
"type": "address",
"name": "out"
}
],
"inputs": [],
"constant": true,
"payable": false,
"type": "function"
},
{
"name": "factoryAddress",
"outputs": [
{
"type": "address",
"name": "out"
}
],
"inputs": [],
"constant": true,
"payable": false,
"type": "function"
},
{
"name": "balanceOf",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "address",
"name": "_owner"
}
],
"constant": true,
"payable": false,
"type": "function"
},
{
"name": "transfer",
"outputs": [
{
"type": "bool",
"name": "out"
}
],
"inputs": [
{
"type": "address",
"name": "_to"
},
{
"type": "uint256",
"name": "_value"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "transferFrom",
"outputs": [
{
"type": "bool",
"name": "out"
}
],
"inputs": [
{
"type": "address",
"name": "_from"
},
{
"type": "address",
"name": "_to"
},
{
"type": "uint256",
"name": "_value"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "approve",
"outputs": [
{
"type": "bool",
"name": "out"
}
],
"inputs": [
{
"type": "address",
"name": "_spender"
},
{
"type": "uint256",
"name": "_value"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "allowance",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "address",
"name": "_owner"
},
{
"type": "address",
"name": "_spender"
}
],
"constant": true,
"payable": false,
"type": "function"
},
{
"name": "name",
"outputs": [
{
"type": "bytes32",
"name": "out"
}
],
"inputs": [],
"constant": true,
"payable": false,
"type": "function"
},
{
"name": "symbol",
"outputs": [
{
"type": "bytes32",
"name": "out"
}
],
"inputs": [],
"constant": true,
"payable": false,
"type": "function"
},
{
"name": "decimals",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [],
"constant": true,
"payable": false,
"type": "function"
},
{
"name": "totalSupply",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [],
"constant": true,
"payable": false,
"type": "function"
}
]

View File

@@ -14,8 +14,7 @@
"inputs": [{ "type": "address", "name": "template" }],
"constant": false,
"payable": false,
"type": "function",
"gas": 35725
"type": "function"
},
{
"name": "createExchange",
@@ -23,8 +22,7 @@
"inputs": [{ "type": "address", "name": "token" }],
"constant": false,
"payable": false,
"type": "function",
"gas": 187911
"type": "function"
},
{
"name": "getExchange",
@@ -32,8 +30,7 @@
"inputs": [{ "type": "address", "name": "token" }],
"constant": true,
"payable": false,
"type": "function",
"gas": 715
"type": "function"
},
{
"name": "getToken",
@@ -41,8 +38,7 @@
"inputs": [{ "type": "address", "name": "exchange" }],
"constant": true,
"payable": false,
"type": "function",
"gas": 745
"type": "function"
},
{
"name": "getTokenWithId",
@@ -50,8 +46,7 @@
"inputs": [{ "type": "uint256", "name": "token_id" }],
"constant": true,
"payable": false,
"type": "function",
"gas": 736
"type": "function"
},
{
"name": "exchangeTemplate",
@@ -59,8 +54,7 @@
"inputs": [],
"constant": true,
"payable": false,
"type": "function",
"gas": 633
"type": "function"
},
{
"name": "tokenCount",
@@ -68,7 +62,6 @@
"inputs": [],
"constant": true,
"payable": false,
"type": "function",
"gas": 663
"type": "function"
}
]

View File

@@ -8,7 +8,7 @@ export function useTokenAllowance(token?: Token, owner?: string, spender?: strin
const contract = useTokenContract(token?.address, false)
const inputs = useMemo(() => [owner, spender], [owner, spender])
const allowance = useSingleCallResult(contract, 'allowance', inputs)
const allowance = useSingleCallResult(contract, 'allowance', inputs).result
return useMemo(() => (token && allowance ? new TokenAmount(token, allowance.toString()) : undefined), [
token,

View File

@@ -12,13 +12,13 @@ import { useSingleCallResult } from '../state/multicall/hooks'
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
const pairAddress = tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
const contract = usePairContract(pairAddress, false)
const reserves = useSingleCallResult(contract, 'getReserves')
const { result: reserves, loading } = useSingleCallResult(contract, 'getReserves')
return useMemo(() => {
if (!pairAddress || !contract || !tokenA || !tokenB) return undefined
if (loading || !tokenA || !tokenB) return undefined
if (!reserves) return null
const { reserve0, reserve1 } = reserves
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
}, [contract, pairAddress, reserves, tokenA, tokenB])
}, [loading, reserves, tokenA, tokenB])
}

View File

@@ -8,7 +8,7 @@ import { useSingleCallResult } from '../state/multicall/hooks'
export function useTotalSupply(token?: Token): TokenAmount | undefined {
const contract = useTokenContract(token?.address, false)
const totalSupply: BigNumber = useSingleCallResult(contract, 'totalSupply')?.[0]
const totalSupply: BigNumber = useSingleCallResult(contract, 'totalSupply')?.result?.[0]
return token && totalSupply ? new TokenAmount(token, totalSupply.toString()) : undefined
}

View File

@@ -1,18 +1,23 @@
import { ChainId, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType, WETH } from '@uniswap/sdk'
import { ChainId, JSBI, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType, WETH } from '@uniswap/sdk'
import { useMemo } from 'react'
import { useActiveWeb3React } from '../hooks'
import { useAllTokens } from '../hooks/Tokens'
import { useV1FactoryContract } from '../hooks/useContract'
import { useSingleCallResult } from '../state/multicall/hooks'
import { useETHBalances, useTokenBalance } from '../state/wallet/hooks'
import { NEVER_RELOAD, useSingleCallResult, useSingleContractMultipleData } from '../state/multicall/hooks'
import { useETHBalances, useTokenBalance, useTokenBalances } from '../state/wallet/hooks'
function useV1PairAddress(tokenAddress?: string): string | undefined {
const contract = useV1FactoryContract()
const inputs = useMemo(() => [tokenAddress], [tokenAddress])
return useSingleCallResult(contract, 'getExchange', inputs)?.[0]
return useSingleCallResult(contract, 'getExchange', inputs)?.result?.[0]
}
function useMockV1Pair(token?: Token) {
class MockV1Pair extends Pair {
readonly isV1: true = true
}
function useMockV1Pair(token?: Token): MockV1Pair | undefined {
const isWETH = token?.equals(WETH[token?.chainId])
// will only return an address on mainnet, and not for WETH
@@ -21,10 +26,57 @@ function useMockV1Pair(token?: Token) {
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? '']
return tokenBalance && ETHBalance && token
? new Pair(tokenBalance, new TokenAmount(WETH[token.chainId], ETHBalance.toString()))
? new MockV1Pair(tokenBalance, new TokenAmount(WETH[token.chainId], ETHBalance.toString()))
: undefined
}
// returns ALL v1 exchange addresses
export function useAllV1ExchangeAddresses(): string[] {
const factory = useV1FactoryContract()
const exchangeCount = useSingleCallResult(factory, 'tokenCount')?.result
const parsedCount = parseInt(exchangeCount?.toString() ?? '0')
const indices = useMemo(() => [...Array(parsedCount).keys()].map(ix => [ix]), [parsedCount])
const data = useSingleContractMultipleData(factory, 'getTokenWithId', indices, NEVER_RELOAD)
return useMemo(() => data?.map(({ result }) => result?.[0])?.filter(x => x) ?? [], [data])
}
// returns all v1 exchange addresses in the user's token list
export function useAllTokenV1ExchangeAddresses(): string[] {
const allTokens = useAllTokens()
const factory = useV1FactoryContract()
const args = useMemo(() => Object.keys(allTokens).map(tokenAddress => [tokenAddress]), [allTokens])
const data = useSingleContractMultipleData(factory, 'getExchange', args, NEVER_RELOAD)
return useMemo(() => data?.map(({ result }) => result?.[0])?.filter(x => x) ?? [], [data])
}
// returns whether any of the tokens in the user's token list have liquidity on v1
export function useUserProbablyHasV1Liquidity(): boolean | undefined {
const exchangeAddresses = useAllTokenV1ExchangeAddresses()
const { account, chainId } = useActiveWeb3React()
const fakeTokens = useMemo(
() => (chainId ? exchangeAddresses.map(address => new Token(chainId, address, 18, 'UNI-V1')) : []),
[chainId, exchangeAddresses]
)
const balances = useTokenBalances(account ?? undefined, fakeTokens)
return useMemo(
() =>
Object.keys(balances).some(tokenAddress => {
const b = balances[tokenAddress]?.raw
return b && JSBI.greaterThan(b, JSBI.BigInt(0))
}),
[balances]
)
}
export function useV1TradeLinkIfBetter(
isExactIn?: boolean,
input?: Token,

View File

@@ -2,9 +2,8 @@ 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 { V1_FACTORY_ADDRESS } from '../constants'
import ERC20_ABI from '../constants/abis/erc20.json'
import IUniswapV1Factory from '../constants/abis/v1_factory.json'
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESS } from '../constants/v1'
import { MULTICALL_ABI, MULTICALL_NETWORKS } from '../constants/multicall'
import { getContract } from '../utils'
import { useActiveWeb3React } from './index'
@@ -25,7 +24,12 @@ function useContract(address?: string, ABI?: any, withSignerIfPossible = true):
}
export function useV1FactoryContract(): Contract | null {
return useContract(V1_FACTORY_ADDRESS, IUniswapV1Factory, false)
const { chainId } = useActiveWeb3React()
return useContract(chainId === 1 ? V1_FACTORY_ADDRESS : undefined, V1_FACTORY_ABI, false)
}
export function useV1ExchangeContract(address: string): Contract | null {
return useContract(address, V1_EXCHANGE_ABI, false)
}
export function useTokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null {

View File

@@ -0,0 +1,40 @@
import { JSBI, Token } from '@uniswap/sdk'
import React, { useMemo } from 'react'
import { RouteComponentProps } from 'react-router'
import { useAllV1ExchangeAddresses } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks'
import { useTokenBalances } from '../../state/wallet/hooks'
const PLACEHOLDER_ACCOUNT = (
<div>
<h1>You must connect a wallet to use this tool.</h1>
</div>
)
/**
* Page component for migrating liquidity from V1
*/
export default function MigrateV1({}: RouteComponentProps) {
const { account, chainId } = useActiveWeb3React()
const v1ExchangeAddresses = useAllV1ExchangeAddresses()
const v1ExchangeTokens: Token[] = useMemo(() => {
return v1ExchangeAddresses.map(exchangeAddress => new Token(chainId, exchangeAddress, 18))
}, [chainId, v1ExchangeAddresses])
const tokenBalances = useTokenBalances(account, v1ExchangeTokens)
const unmigratedExchangeAddresses = useMemo(
() =>
Object.keys(tokenBalances).filter(tokenAddress =>
tokenBalances[tokenAddress] ? JSBI.greaterThan(tokenBalances[tokenAddress]?.raw, JSBI.BigInt(0)) : false
),
[tokenBalances]
)
if (!account) {
return PLACEHOLDER_ACCOUNT
}
return <div>{unmigratedExchangeAddresses?.join('\n')}</div>
}

View File

@@ -6,6 +6,7 @@ import { RouteComponentProps } from 'react-router-dom'
import Question from '../../components/QuestionHelper'
import SearchModal from '../../components/SearchModal'
import PositionCard from '../../components/PositionCard'
import { useUserProbablyHasV1Liquidity } from '../../data/V1'
import { useTokenBalances } from '../../state/wallet/hooks'
import { Link, TYPE } from '../../theme'
import { Text } from 'rebass'
@@ -58,6 +59,8 @@ export default function Pool({ history }: RouteComponentProps) {
return <PositionCardWrapper key={i} dummyPair={pair} />
})
const hasV1Liquidity = useUserProbablyHasV1Liquidity()
return (
<AppBody>
<AutoColumn gap="lg" justify="center">
@@ -92,15 +95,23 @@ export default function Pool({ history }: RouteComponentProps) {
)}
{filteredExchangeList}
<Text textAlign="center" fontSize={14} style={{ padding: '.5rem 0 .5rem 0' }}>
{filteredExchangeList?.length !== 0 ? `Don't see a pool you joined? ` : 'Already joined a pool? '}{' '}
<Link
id="import-pool-link"
onClick={() => {
history.push('/find')
}}
>
Import it.
</Link>
{!hasV1Liquidity ? (
<>
{filteredExchangeList?.length !== 0 ? `Don't see a pool you joined? ` : 'Already joined a pool? '}{' '}
<Link
id="import-pool-link"
onClick={() => {
history.push('/find')
}}
>
Import it.
</Link>
</>
) : (
<Link id="migrate-v1-liquidity-link" href="https://migrate.uniswap.exchange">
Migrate your V1 liquidity.
</Link>
)}
</Text>
</AutoColumn>
<FixedBottom>

View File

@@ -30,8 +30,25 @@ export function parseCallKey(callKey: string): Call {
}
}
export const addMulticallListeners = createAction<{ chainId: number; calls: Call[] }>('addMulticallListeners')
export const removeMulticallListeners = createAction<{ chainId: number; calls: Call[] }>('removeMulticallListeners')
export interface ListenerOptions {
// how often this data should be fetched, by default 1
readonly blocksPerFetch?: number
}
export const addMulticallListeners = createAction<{ chainId: number; calls: Call[]; options?: ListenerOptions }>(
'addMulticallListeners'
)
export const removeMulticallListeners = createAction<{ chainId: number; calls: Call[]; options?: ListenerOptions }>(
'removeMulticallListeners'
)
export const fetchingMulticallResults = createAction<{ chainId: number; calls: Call[]; fetchingBlockNumber: number }>(
'fetchingMulticallResults'
)
export const errorFetchingMulticallResults = createAction<{
chainId: number
calls: Call[]
fetchingBlockNumber: number
}>('errorFetchingMulticallResults')
export const updateMulticallResults = createAction<{
chainId: number
blockNumber: number

View File

@@ -1,12 +1,19 @@
import { Interface } from '@ethersproject/abi'
import { Interface, FunctionFragment } from '@ethersproject/abi'
import { BigNumber } from '@ethersproject/bignumber'
import { Contract } from '@ethersproject/contracts'
import { useEffect, useMemo } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useActiveWeb3React } from '../../hooks'
import useDebounce from '../../hooks/useDebounce'
import { useBlockNumber } from '../application/hooks'
import { AppDispatch, AppState } from '../index'
import { addMulticallListeners, Call, removeMulticallListeners, parseCallKey, toCallKey } from './actions'
import {
addMulticallListeners,
Call,
removeMulticallListeners,
parseCallKey,
toCallKey,
ListenerOptions
} from './actions'
export interface Result extends ReadonlyArray<any> {
readonly [key: string]: any
@@ -27,8 +34,21 @@ function isValidMethodArgs(x: unknown): x is MethodArgs | undefined {
)
}
interface CallResult {
readonly valid: boolean
readonly data: string | undefined
readonly blockNumber: number | undefined
}
const INVALID_RESULT: CallResult = { valid: false, blockNumber: undefined, data: undefined }
// use this options object
export const NEVER_RELOAD: ListenerOptions = {
blocksPerFetch: Infinity
}
// the lowest level call for subscribing to contract data
function useCallsData(calls: (Call | undefined)[]): (string | undefined)[] {
function useCallsData(calls: (Call | undefined)[], options?: ListenerOptions): CallResult[] {
const { chainId } = useActiveWeb3React()
const callResults = useSelector<AppState, AppState['multicall']['callResults']>(state => state.multicall.callResults)
const dispatch = useDispatch<AppDispatch>()
@@ -44,17 +64,16 @@ function useCallsData(calls: (Call | undefined)[]): (string | undefined)[] {
[calls]
)
const debouncedSerializedCallKeys = useDebounce(serializedCallKeys, 20)
// update listeners when there is an actual change that persists for at least 100ms
useEffect(() => {
const callKeys: string[] = JSON.parse(debouncedSerializedCallKeys)
const callKeys: string[] = JSON.parse(serializedCallKeys)
if (!chainId || callKeys.length === 0) return
const calls = callKeys.map(key => parseCallKey(key))
dispatch(
addMulticallListeners({
chainId,
calls
calls,
options
})
)
@@ -62,31 +81,72 @@ function useCallsData(calls: (Call | undefined)[]): (string | undefined)[] {
dispatch(
removeMulticallListeners({
chainId,
calls
calls,
options
})
)
}
}, [chainId, dispatch, debouncedSerializedCallKeys])
}, [chainId, dispatch, options, serializedCallKeys])
return useMemo(() => {
return calls.map<string | undefined>(call => {
if (!chainId || !call) return undefined
return useMemo(
() =>
calls.map<CallResult>(call => {
if (!chainId || !call) return INVALID_RESULT
const result = callResults[chainId]?.[toCallKey(call)]
if (!result || !result.data || result.data === '0x') {
return undefined
}
const result = callResults[chainId]?.[toCallKey(call)]
let data
if (result?.data && result?.data !== '0x') {
data = result.data
}
return result.data
})
}, [callResults, calls, chainId])
return { valid: true, data, blockNumber: result?.blockNumber }
}),
[callResults, calls, chainId]
)
}
interface CallState {
readonly valid: boolean
// the result, or undefined if loading or errored/no data
readonly result: Result | undefined
// true if the result has never been fetched
readonly loading: boolean
// true if the result is not for the latest block
readonly syncing: boolean
// true if the call was made and is synced, but the return data is invalid
readonly error: boolean
}
const INVALID_CALL_STATE: CallState = { valid: false, result: undefined, loading: false, syncing: false, error: false }
const LOADING_CALL_STATE: CallState = { valid: true, result: undefined, loading: true, syncing: true, error: false }
function toCallState(
result: CallResult | undefined,
contractInterface: Interface | undefined,
fragment: FunctionFragment | undefined,
latestBlockNumber: number | undefined
): CallState {
if (!result) return INVALID_CALL_STATE
const { valid, data, blockNumber } = result
if (!valid) return INVALID_CALL_STATE
if (valid && !blockNumber) return LOADING_CALL_STATE
if (!contractInterface || !fragment || !latestBlockNumber) return LOADING_CALL_STATE
const success = data && data.length > 2
return {
valid: true,
loading: false,
syncing: (blockNumber ?? 0) < latestBlockNumber,
result: success && data ? contractInterface.decodeFunctionResult(fragment, data) : undefined,
error: !success
}
}
export function useSingleContractMultipleData(
contract: Contract | null | undefined,
methodName: string,
callInputs: OptionalMethodInputs[]
): (Result | undefined)[] {
callInputs: OptionalMethodInputs[],
options?: ListenerOptions
): CallState[] {
const fragment = useMemo(() => contract?.interface?.getFunction(methodName), [contract, methodName])
const calls = useMemo(
@@ -102,20 +162,22 @@ export function useSingleContractMultipleData(
[callInputs, contract, fragment]
)
const data = useCallsData(calls)
const results = useCallsData(calls, options)
const latestBlockNumber = useBlockNumber()
return useMemo(() => {
if (!fragment || !contract) return []
return data.map(data => (data ? contract.interface.decodeFunctionResult(fragment, data) : undefined))
}, [contract, data, fragment])
return results.map(result => toCallState(result, contract?.interface, fragment, latestBlockNumber))
}, [fragment, contract, results, latestBlockNumber])
}
export function useMultipleContractSingleData(
addresses: (string | undefined)[],
contractInterface: Interface,
methodName: string,
callInputs?: OptionalMethodInputs
): (Result | undefined)[] {
callInputs?: OptionalMethodInputs,
options?: ListenerOptions
): CallState[] {
const fragment = useMemo(() => contractInterface.getFunction(methodName), [contractInterface, methodName])
const callData: string | undefined = useMemo(
() =>
@@ -140,19 +202,21 @@ export function useMultipleContractSingleData(
[addresses, callData, fragment]
)
const data = useCallsData(calls)
const results = useCallsData(calls, options)
const latestBlockNumber = useBlockNumber()
return useMemo(() => {
if (!fragment) return []
return data.map(data => (data ? contractInterface.decodeFunctionResult(fragment, data) : undefined))
}, [contractInterface, data, fragment])
return results.map(result => toCallState(result, contractInterface, fragment, latestBlockNumber))
}, [fragment, results, contractInterface, latestBlockNumber])
}
export function useSingleCallResult(
contract: Contract | null | undefined,
methodName: string,
inputs?: OptionalMethodInputs
): Result | undefined {
inputs?: OptionalMethodInputs,
options?: ListenerOptions
): CallState {
const fragment = useMemo(() => contract?.interface?.getFunction(methodName), [contract, methodName])
const calls = useMemo<Call[]>(() => {
@@ -166,9 +230,10 @@ export function useSingleCallResult(
: []
}, [contract, fragment, inputs])
const data = useCallsData(calls)[0]
const result = useCallsData(calls, options)[0]
const latestBlockNumber = useBlockNumber()
return useMemo(() => {
if (!contract || !fragment || !data) return undefined
return contract.interface.decodeFunctionResult(fragment, data)
}, [data, fragment, contract])
return toCallState(result, contract?.interface, fragment, latestBlockNumber)
}, [result, contract, fragment, latestBlockNumber])
}

View File

@@ -0,0 +1,167 @@
import { addMulticallListeners, removeMulticallListeners, updateMulticallResults } from './actions'
import reducer, { MulticallState } from './reducer'
import { Store, createStore } from '@reduxjs/toolkit'
describe('multicall reducer', () => {
let store: Store<MulticallState>
beforeEach(() => {
store = createStore(reducer)
})
it('has correct initial state', () => {
expect(store.getState().callResults).toEqual({})
expect(store.getState().callListeners).toEqual(undefined)
})
describe('addMulticallListeners', () => {
it('adds listeners', () => {
store.dispatch(
addMulticallListeners({
chainId: 1,
calls: [
{
address: '0x',
callData: '0x'
}
]
})
)
expect(store.getState()).toEqual({
callListeners: {
[1]: {
'0x-0x': {
[1]: 1
}
}
},
callResults: {}
})
})
})
describe('removeMulticallListeners', () => {
it('noop', () => {
store.dispatch(
removeMulticallListeners({
calls: [
{
address: '0x',
callData: '0x'
}
],
chainId: 1
})
)
expect(store.getState()).toEqual({ callResults: {}, callListeners: {} })
})
it('removes listeners', () => {
store.dispatch(
addMulticallListeners({
chainId: 1,
calls: [
{
address: '0x',
callData: '0x'
}
]
})
)
store.dispatch(
removeMulticallListeners({
calls: [
{
address: '0x',
callData: '0x'
}
],
chainId: 1
})
)
expect(store.getState()).toEqual({ callResults: {}, callListeners: { [1]: { '0x-0x': {} } } })
})
})
describe('updateMulticallResults', () => {
it('updates data if not present', () => {
store.dispatch(
updateMulticallResults({
chainId: 1,
blockNumber: 1,
results: {
abc: '0x'
}
})
)
expect(store.getState()).toEqual({
callResults: {
[1]: {
abc: {
blockNumber: 1,
data: '0x'
}
}
}
})
})
it('updates old data', () => {
store.dispatch(
updateMulticallResults({
chainId: 1,
blockNumber: 1,
results: {
abc: '0x'
}
})
)
store.dispatch(
updateMulticallResults({
chainId: 1,
blockNumber: 2,
results: {
abc: '0x2'
}
})
)
expect(store.getState()).toEqual({
callResults: {
[1]: {
abc: {
blockNumber: 2,
data: '0x2'
}
}
}
})
})
it('ignores late updates', () => {
store.dispatch(
updateMulticallResults({
chainId: 1,
blockNumber: 2,
results: {
abc: '0x2'
}
})
)
store.dispatch(
updateMulticallResults({
chainId: 1,
blockNumber: 1,
results: {
abc: '0x1'
}
})
)
expect(store.getState()).toEqual({
callResults: {
[1]: {
abc: {
blockNumber: 2,
data: '0x2'
}
}
}
})
})
})
})

View File

@@ -1,53 +1,103 @@
import { createReducer } from '@reduxjs/toolkit'
import { addMulticallListeners, removeMulticallListeners, toCallKey, updateMulticallResults } from './actions'
import {
addMulticallListeners,
errorFetchingMulticallResults,
fetchingMulticallResults,
removeMulticallListeners,
toCallKey,
updateMulticallResults
} from './actions'
interface MulticallState {
callListeners: {
export interface MulticallState {
callListeners?: {
// on a per-chain basis
[chainId: number]: {
[callKey: string]: number
// stores for each call key the listeners' preferences
[callKey: string]: {
// stores how many listeners there are per each blocks per fetch preference
[blocksPerFetch: number]: number
}
}
}
callResults: {
[chainId: number]: {
[callKey: string]: {
data: string | null
blockNumber: number
data?: string | null
blockNumber?: number
fetchingBlockNumber?: number
}
}
}
}
const initialState: MulticallState = {
callListeners: {},
callResults: {}
}
export default createReducer(initialState, builder =>
builder
.addCase(addMulticallListeners, (state, { payload: { calls, chainId } }) => {
state.callListeners[chainId] = state.callListeners[chainId] ?? {}
.addCase(addMulticallListeners, (state, { payload: { calls, chainId, options: { blocksPerFetch = 1 } = {} } }) => {
const listeners: MulticallState['callListeners'] = state.callListeners
? state.callListeners
: (state.callListeners = {})
listeners[chainId] = listeners[chainId] ?? {}
calls.forEach(call => {
const callKey = toCallKey(call)
state.callListeners[chainId][callKey] = (state.callListeners[chainId][callKey] ?? 0) + 1
listeners[chainId][callKey] = listeners[chainId][callKey] ?? {}
listeners[chainId][callKey][blocksPerFetch] = (listeners[chainId][callKey][blocksPerFetch] ?? 0) + 1
})
})
.addCase(removeMulticallListeners, (state, { payload: { chainId, calls } }) => {
if (!state.callListeners[chainId]) return
.addCase(
removeMulticallListeners,
(state, { payload: { chainId, calls, options: { blocksPerFetch = 1 } = {} } }) => {
const listeners: MulticallState['callListeners'] = state.callListeners
? state.callListeners
: (state.callListeners = {})
if (!listeners[chainId]) return
calls.forEach(call => {
const callKey = toCallKey(call)
if (!listeners[chainId][callKey]) return
if (!listeners[chainId][callKey][blocksPerFetch]) return
if (listeners[chainId][callKey][blocksPerFetch] === 1) {
delete listeners[chainId][callKey][blocksPerFetch]
} else {
listeners[chainId][callKey][blocksPerFetch]--
}
})
}
)
.addCase(fetchingMulticallResults, (state, { payload: { chainId, fetchingBlockNumber, calls } }) => {
state.callResults[chainId] = state.callResults[chainId] ?? {}
calls.forEach(call => {
const callKey = toCallKey(call)
if (state.callListeners[chainId][callKey] === 1) {
delete state.callListeners[chainId][callKey]
const current = state.callResults[chainId][callKey]
if (!current) {
state.callResults[chainId][callKey] = {
fetchingBlockNumber
}
} else {
state.callListeners[chainId][callKey]--
if (current.fetchingBlockNumber ?? 0 >= fetchingBlockNumber) return
state.callResults[chainId][callKey].fetchingBlockNumber = fetchingBlockNumber
}
})
})
.addCase(errorFetchingMulticallResults, (state, { payload: { fetchingBlockNumber, chainId, calls } }) => {
state.callResults[chainId] = state.callResults[chainId] ?? {}
calls.forEach(call => {
const callKey = toCallKey(call)
const current = state.callResults[chainId][callKey]
if (current && current.fetchingBlockNumber !== fetchingBlockNumber) return
delete current.fetchingBlockNumber
})
})
.addCase(updateMulticallResults, (state, { payload: { chainId, results, blockNumber } }) => {
state.callResults[chainId] = state.callResults[chainId] ?? {}
Object.keys(results).forEach(callKey => {
const current = state.callResults[chainId][callKey]
if (current && current.blockNumber > blockNumber) return
if ((current?.blockNumber ?? 0) > blockNumber) return
state.callResults[chainId][callKey] = {
data: results[callKey],
blockNumber

View File

@@ -0,0 +1,168 @@
import { activeListeningKeys, outdatedListeningKeys } from './updater'
describe('multicall updater', () => {
describe('#activeListeningKeys', () => {
it('ignores 0, returns call key to block age key', () => {
expect(
activeListeningKeys(
{
[1]: {
['abc']: {
4: 2, // 2 listeners care about 4 block old data
1: 0 // 0 listeners care about 1 block old data
}
}
},
1
)
).toEqual({
abc: 4
})
})
it('applies min', () => {
expect(
activeListeningKeys(
{
[1]: {
['abc']: {
4: 2, // 2 listeners care about 4 block old data
3: 1, // 1 listener cares about 3 block old data
1: 0 // 0 listeners care about 1 block old data
}
}
},
1
)
).toEqual({
abc: 3
})
})
it('works for infinity', () => {
expect(
activeListeningKeys(
{
[1]: {
['abc']: {
4: 2, // 2 listeners care about 4 block old data
1: 0 // 0 listeners care about 1 block old data
},
['def']: {
Infinity: 2
}
}
},
1
)
).toEqual({
abc: 4,
def: Infinity
})
})
it('multiple keys', () => {
expect(
activeListeningKeys(
{
[1]: {
['abc']: {
4: 2, // 2 listeners care about 4 block old data
1: 0 // 0 listeners care about 1 block old data
},
['def']: {
2: 1,
5: 2
}
}
},
1
)
).toEqual({
abc: 4,
def: 2
})
})
it('ignores negative numbers', () => {
expect(
activeListeningKeys(
{
[1]: {
['abc']: {
4: 2,
1: -1,
[-3]: 4
}
}
},
1
)
).toEqual({
abc: 4
})
})
it('applies min to infinity', () => {
expect(
activeListeningKeys(
{
[1]: {
['abc']: {
Infinity: 2, // 2 listeners care about any data
4: 2, // 2 listeners care about 4 block old data
1: 0 // 0 listeners care about 1 block old data
}
}
},
1
)
).toEqual({
abc: 4
})
})
})
describe('#outdatedListeningKeys', () => {
it('returns empty if missing block number or chain id', () => {
expect(outdatedListeningKeys({}, { abc: 2 }, undefined, undefined)).toEqual([])
expect(outdatedListeningKeys({}, { abc: 2 }, 1, undefined)).toEqual([])
expect(outdatedListeningKeys({}, { abc: 2 }, undefined, 1)).toEqual([])
})
it('returns everything for no results', () => {
expect(outdatedListeningKeys({}, { abc: 2, def: 3 }, 1, 1)).toEqual(['abc', 'def'])
})
it('returns only outdated keys', () => {
expect(
outdatedListeningKeys({ [1]: { abc: { data: '0x', blockNumber: 2 } } }, { abc: 1, def: 1 }, 1, 2)
).toEqual(['def'])
})
it('returns only keys not being fetched', () => {
expect(
outdatedListeningKeys(
{
[1]: { abc: { data: '0x', blockNumber: 2 }, def: { fetchingBlockNumber: 2 } }
},
{ abc: 1, def: 1 },
1,
2
)
).toEqual([])
})
it('returns keys being fetched for old blocks', () => {
expect(
outdatedListeningKeys(
{ [1]: { abc: { data: '0x', blockNumber: 2 }, def: { fetchingBlockNumber: 1 } } },
{ abc: 1, def: 1 },
1,
2
)
).toEqual(['def'])
})
it('respects blocks per fetch', () => {
expect(
outdatedListeningKeys(
{ [1]: { abc: { data: '0x', blockNumber: 2 }, def: { data: '0x', fetchingBlockNumber: 1 } } },
{ abc: 2, def: 2 },
1,
3
)
).toEqual(['def'])
})
})
})

View File

@@ -7,48 +7,118 @@ import useDebounce from '../../hooks/useDebounce'
import chunkArray from '../../utils/chunkArray'
import { useBlockNumber } from '../application/hooks'
import { AppDispatch, AppState } from '../index'
import { parseCallKey, updateMulticallResults } from './actions'
import {
errorFetchingMulticallResults,
fetchingMulticallResults,
parseCallKey,
updateMulticallResults
} from './actions'
// chunk calls so we do not exceed the gas limit
const CALL_CHUNK_SIZE = 250
const CALL_CHUNK_SIZE = 500
/**
* From the current all listeners state, return each call key mapped to the
* minimum number of blocks per fetch. This is how often each key must be fetched.
* @param allListeners the all listeners state
* @param chainId the current chain id
*/
export function activeListeningKeys(
allListeners: AppState['multicall']['callListeners'],
chainId?: number
): { [callKey: string]: number } {
if (!allListeners || !chainId) return {}
const listeners = allListeners[chainId]
if (!listeners) return {}
return Object.keys(listeners).reduce<{ [callKey: string]: number }>((memo, callKey) => {
const keyListeners = listeners[callKey]
memo[callKey] = Object.keys(keyListeners)
.filter(key => {
const blocksPerFetch = parseInt(key)
if (blocksPerFetch <= 0) return false
return keyListeners[blocksPerFetch] > 0
})
.reduce((previousMin, current) => {
return Math.min(previousMin, parseInt(current))
}, Infinity)
return memo
}, {})
}
/**
* Return the keys that need to be refetched
* @param callResults current call result state
* @param listeningKeys each call key mapped to how old the data can be in blocks
* @param chainId the current chain id
* @param latestBlockNumber the latest block number
*/
export function outdatedListeningKeys(
callResults: AppState['multicall']['callResults'],
listeningKeys: { [callKey: string]: number },
chainId: number | undefined,
latestBlockNumber: number | undefined
): string[] {
if (!chainId || !latestBlockNumber) return []
const results = callResults[chainId]
// no results at all, load everything
if (!results) return Object.keys(listeningKeys)
return Object.keys(listeningKeys).filter(callKey => {
const blocksPerFetch = listeningKeys[callKey]
const data = callResults[chainId][callKey]
// no data, must fetch
if (!data) return true
const minDataBlockNumber = latestBlockNumber - (blocksPerFetch - 1)
// already fetching it for a recent enough block, don't refetch it
if (data.fetchingBlockNumber && data.fetchingBlockNumber >= minDataBlockNumber) return false
// if data is newer than minDataBlockNumber, don't fetch it
return !(data.blockNumber && data.blockNumber >= minDataBlockNumber)
})
}
export default function Updater() {
const dispatch = useDispatch<AppDispatch>()
const state = useSelector<AppState, AppState['multicall']>(state => state.multicall)
// wait for listeners to settle before triggering updates
const debouncedListeners = useDebounce(state.callListeners, 100)
const latestBlockNumber = useBlockNumber()
const { chainId } = useActiveWeb3React()
const multicallContract = useMulticallContract()
const listeningKeys = useMemo(() => {
if (!chainId || !state.callListeners[chainId]) return []
return Object.keys(state.callListeners[chainId]).filter(callKey => state.callListeners[chainId][callKey] > 0)
}, [state.callListeners, chainId])
const debouncedResults = useDebounce(state.callResults, 20)
const debouncedListeningKeys = useDebounce(listeningKeys, 20)
const listeningKeys: { [callKey: string]: number } = useMemo(() => {
return activeListeningKeys(debouncedListeners, chainId)
}, [debouncedListeners, chainId])
const unserializedOutdatedCallKeys = useMemo(() => {
if (!chainId || !debouncedResults[chainId]) return debouncedListeningKeys
if (!latestBlockNumber) return []
return debouncedListeningKeys.filter(key => {
const data = debouncedResults[chainId][key]
return !data || data.blockNumber < latestBlockNumber
})
}, [chainId, debouncedResults, debouncedListeningKeys, latestBlockNumber])
return outdatedListeningKeys(state.callResults, listeningKeys, chainId, latestBlockNumber)
}, [chainId, state.callResults, listeningKeys, latestBlockNumber])
const serializedOutdatedCallKeys = useMemo(() => JSON.stringify(unserializedOutdatedCallKeys.sort()), [
unserializedOutdatedCallKeys
])
useEffect(() => {
if (!latestBlockNumber || !chainId || !multicallContract) return
const outdatedCallKeys: string[] = JSON.parse(serializedOutdatedCallKeys)
if (!multicallContract || !chainId || outdatedCallKeys.length === 0) return
if (outdatedCallKeys.length === 0) return
const calls = outdatedCallKeys.map(key => parseCallKey(key))
const chunkedCalls = chunkArray(calls, CALL_CHUNK_SIZE)
console.debug('Firing off chunked calls', chunkedCalls)
dispatch(
fetchingMulticallResults({
calls,
chainId,
fetchingBlockNumber: latestBlockNumber
})
)
chunkedCalls.forEach((chunk, index) =>
multicallContract
@@ -73,9 +143,16 @@ export default function Updater() {
})
.catch((error: any) => {
console.error('Failed to fetch multicall chunk', chunk, chainId, error)
dispatch(
errorFetchingMulticallResults({
calls: chunk,
chainId,
fetchingBlockNumber: latestBlockNumber
})
)
})
)
}, [chainId, multicallContract, dispatch, serializedOutdatedCallKeys])
}, [chainId, multicallContract, dispatch, serializedOutdatedCallKeys, latestBlockNumber])
return null
}

View File

@@ -33,7 +33,7 @@ export function useETHBalances(uncheckedAddresses?: (string | undefined)[]): { [
return useMemo(
() =>
addresses.reduce<{ [address: string]: JSBI | undefined }>((memo, address, i) => {
const value = results?.[i]?.[0]
const value = results?.[i]?.result?.[0]
if (value) memo[address] = JSBI.BigInt(value.toString())
return memo
}, {}),
@@ -61,7 +61,7 @@ export function useTokenBalances(
() =>
address && validatedTokens.length > 0
? validatedTokens.reduce<{ [tokenAddress: string]: TokenAmount | undefined }>((memo, token, i) => {
const value = balances?.[i]?.[0]
const value = balances?.[i]?.result?.[0]
const amount = value ? JSBI.BigInt(value.toString()) : undefined
if (amount) {
memo[token.address] = new TokenAmount(token, amount)

3179
yarn.lock

File diff suppressed because it is too large Load Diff