Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f289dec684 | ||
|
|
73d3df05f2 | ||
|
|
83554f44f8 | ||
|
|
320b2e384b | ||
|
|
9492e7375a | ||
|
|
8a6a10be9d | ||
|
|
5e486fca7f | ||
|
|
87d24c404b | ||
|
|
d4011f73d1 | ||
|
|
6fc3157977 |
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
105
src/connectors/NetworkConnector.ts
Normal file
105
src/connectors/NetworkConnector.ts
Normal 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
1
src/connectors/fortmatic.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'formatic'
|
||||
@@ -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]
|
||||
})
|
||||
|
||||
|
||||
4
src/connectors/tsconfig.json
Normal file
4
src/connectors/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.strict.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
9
src/constants/v1/index.ts
Normal file
9
src/constants/v1/index.ts
Normal 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 }
|
||||
971
src/constants/v1/v1_exchange.json
Normal file
971
src/constants/v1/v1_exchange.json
Normal 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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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,
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
40
src/pages/MigrateV1/index.tsx
Normal file
40
src/pages/MigrateV1/index.tsx
Normal 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>
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
167
src/state/multicall/reducer.test.ts
Normal file
167
src/state/multicall/reducer.test.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
|
||||
168
src/state/multicall/updater.test.ts
Normal file
168
src/state/multicall/updater.test.ts
Normal 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'])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user