Commit 2f0e2fb2 authored by Noah Zinsmeister's avatar Noah Zinsmeister Committed by GitHub

Beta cleanup (#285)

* improve context logic

* fix font-variant

* add dependencies for later

* fix icon
parent 8f2c347d
{
"extends": "react-app"
}
......@@ -5,6 +5,7 @@
"homepage": ".",
"private": true,
"dependencies": {
"@reach/tooltip": "^0.2.0",
"classnames": "^2.2.6",
"escape-string-regexp": "^2.0.0",
"ethers": "^4.0.27",
......@@ -12,15 +13,16 @@
"i18next-browser-languagedetector": "^3.0.1",
"i18next-xhr-backend": "^2.0.1",
"jazzicon": "^1.5.0",
"lodash.merge": "^4.6.1",
"node-sass": "^4.11.0",
"react": "^16.8.6",
"react-aria-modal": "^4.0.0",
"react-dom": "^16.8.6",
"react-ga": "^2.5.7",
"react-i18next": "^10.7.0",
"react-router-dom": "^5.0.0",
"react-scripts": "^3.0.0",
"react-scripts": "^3.0.1",
"react-transition-group": "1.x",
"styled-components": "^4.2.0",
"ua-parser-js": "^0.7.18",
"web3-react": "^5.0.4"
},
......@@ -40,14 +42,23 @@
"check:format": "yarn format:base --check",
"check:all": "yarn check:lint && yarn check:format"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"license": "GPL-3.0-or-later",
"devDependencies": {
"prettier": "^1.17.0"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
}
......@@ -11,8 +11,8 @@ import { isAddress, calculateGasMargin } from '../../utils'
import Modal from '../Modal'
import TokenLogo from '../TokenLogo'
import SearchIcon from '../../assets/images/magnifying-glass.svg'
import { useTokenDetails, useAllTokenDetails } from '../../contexts/Static'
import { useTransactionContext, usePendingApproval } from '../../contexts/Transaction'
import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions'
import { useTokenDetails, useAllTokenDetails } from '../../contexts/Tokens'
import './currency-panel.scss'
......@@ -43,7 +43,7 @@ export default function CurrencyInputPanel({
const pendingApproval = usePendingApproval(selectedTokenAddress)
const { addTransaction } = useTransactionContext()
const addTransaction = useTransactionAdder()
const inputRef = useRef()
const allTokens = useAllTokenDetails()
......@@ -74,7 +74,7 @@ export default function CurrencyInputPanel({
gasLimit: calculateGasMargin(estimatedGas, GAS_MARGIN)
})
.then(response => {
addTransaction(response.hash, response)
addTransaction(response)
})
}}
>
......
......@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
import { useBodyKeyDown } from '../../hooks'
import './navigation-tabs.scss'
import { useApplicationContext } from '../../contexts/Application'
import { useBetaMessageManager } from '../../contexts/Application'
const tabOrder = [
{
......@@ -28,7 +28,7 @@ const tabOrder = [
function NavigationTabs({ location: { pathname }, history }) {
const { t } = useTranslation()
const { showBetaMessage, dismissBetaMessage } = useApplicationContext()
const [showBetaMessage, dismissBetaMessage] = useBetaMessageManager()
const navigate = useCallback(
direction => {
......
......@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'
import { ethers } from 'ethers'
import Modal from '../Modal'
import { useAllTransactions } from '../../contexts/Transaction'
import { useAllTransactions } from '../../contexts/Transactions'
import './web3-status.scss'
......@@ -116,7 +116,15 @@ export default function Web3Status() {
return
} else {
el.innerHTML = ''
el.appendChild(Jazzicon(16, parseInt(account.slice(2), 16)))
el.appendChild(
Jazzicon(
16,
ethers.utils
.bigNumberify(account)
.mod(Number.MAX_SAFE_INTEGER)
.toNumber()
)
)
}
}}
/>
......
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { safeAccess, isAddress, getTokenAllowance } from '../utils'
import { useBlockNumber } from './Application'
const UPDATE = 'UPDATE'
const AllowancesContext = createContext()
function useAllowancesContext() {
return useContext(AllowancesContext)
}
function reducer(state, { type, payload }) {
switch (type) {
case UPDATE: {
const { networkId, address, tokenAddress, spenderAddress, value, blockNumber } = payload
return {
...state,
[networkId]: {
...(safeAccess(state, [networkId]) || {}),
[address]: {
...(safeAccess(state, [networkId, address]) || {}),
[tokenAddress]: {
...(safeAccess(state, [networkId, address, tokenAddress]) || {}),
[spenderAddress]: {
value,
blockNumber
}
}
}
}
}
}
default: {
throw Error(`Unexpected action type in AllowancesContext reducer: '${type}'.`)
}
}
}
export default function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, {})
const update = useCallback((networkId, address, tokenAddress, spenderAddress, value, blockNumber) => {
dispatch({ type: UPDATE, payload: { networkId, address, tokenAddress, spenderAddress, value, blockNumber } })
}, [])
const contextValue = useMemo(() => [state, { update }], [state, update])
return <AllowancesContext.Provider value={contextValue}>{children}</AllowancesContext.Provider>
}
export function useAddressAllowance(address, tokenAddress, spenderAddress) {
const { networkId, library } = useWeb3Context()
const globalBlockNumber = useBlockNumber()
const [state, { update }] = useAllowancesContext()
const { value, blockNumber } = safeAccess(state, [networkId, address, tokenAddress, spenderAddress]) || {}
useEffect(() => {
if (
isAddress(address) &&
isAddress(tokenAddress) &&
isAddress(spenderAddress) &&
(value === undefined || blockNumber !== globalBlockNumber) &&
(networkId || networkId === 0) &&
library
) {
let stale = false
getTokenAllowance(address, tokenAddress, spenderAddress, library)
.then(value => {
if (!stale) {
update(networkId, address, tokenAddress, spenderAddress, value, globalBlockNumber)
}
})
.catch(() => {
if (!stale) {
update(networkId, address, tokenAddress, spenderAddress, null, globalBlockNumber)
}
})
return () => {
stale = true
}
}
}, [address, tokenAddress, spenderAddress, value, blockNumber, globalBlockNumber, networkId, library, update])
return value
}
import React, { Component, createContext, useContext, useEffect } from 'react'
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { safeAccess } from '../utils'
import { useBlockEffect } from '../hooks'
const SHOW_BETA_MESSAGE = 'SHOW_BETA_MESSAGE'
const BLOCK_NUMBERS = 'BLOCK_NUMBERS'
const DISMISS_BETA_MESSAGE = 'DISMISS_BETA_MESSAGE'
const UPDATE_BLOCK_NUMBER = 'UPDATE_BLOCK_NUMBER'
const ApplicationContext = createContext()
export default class Provider extends Component {
constructor(props) {
super(props)
function useApplicationContext() {
return useContext(ApplicationContext)
}
this.dismissBetaMessage = () => {
this.setState({ showBetaMessage: false })
function reducer(state, { type, payload }) {
switch (type) {
case DISMISS_BETA_MESSAGE: {
return {
...state,
[SHOW_BETA_MESSAGE]: false
}
}
this.updateBlockNumber = blockNumber => {
this.setState({ blockNumber })
case UPDATE_BLOCK_NUMBER: {
const { networkId, blockNumber } = payload
return {
...state,
[BLOCK_NUMBERS]: {
...(safeAccess(state, [BLOCK_NUMBERS]) || {}),
[networkId]: blockNumber
}
}
}
this.state = {
showBetaMessage: true,
dismissBetaMessage: this.dismissBetaMessage,
blockNumber: undefined,
updateBlockNumber: this.updateBlockNumber
default: {
throw Error(`Unexpected action type in ApplicationContext reducer: '${type}'.`)
}
}
render() {
return <ApplicationContext.Provider value={this.state}>{this.props.children}</ApplicationContext.Provider>
}
}
export function useApplicationContext() {
return useContext(ApplicationContext)
export default function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, {
[SHOW_BETA_MESSAGE]: true,
[BLOCK_NUMBERS]: {}
})
const dismissBetaMessage = useCallback(() => {
dispatch({ type: DISMISS_BETA_MESSAGE })
}, [])
const updateBlockNumber = useCallback((networkId, blockNumber) => {
dispatch({ type: UPDATE_BLOCK_NUMBER, payload: { networkId, blockNumber } })
}, [])
const contextValue = useMemo(() => [state, { dismissBetaMessage, updateBlockNumber }], [
state,
dismissBetaMessage,
updateBlockNumber
])
return <ApplicationContext.Provider value={contextValue}>{children}</ApplicationContext.Provider>
}
export function Updater() {
const { library } = useWeb3Context()
const { updateBlockNumber } = useApplicationContext()
const { networkId, library } = useWeb3Context()
const [, { updateBlockNumber }] = useApplicationContext()
// fetch the block number once on load...
useEffect(() => {
if (library) {
if ((networkId || networkId === 0) && library) {
let stale = false
library
.getBlockNumber()
.then(blockNumber => {
if (!stale) {
updateBlockNumber(blockNumber)
}
})
.catch(() => {
if (!stale) {
updateBlockNumber(null)
}
})
function update() {
library
.getBlockNumber()
.then(blockNumber => {
if (!stale) {
updateBlockNumber(networkId, blockNumber)
}
})
.catch(() => {
if (!stale) {
updateBlockNumber(networkId, null)
}
})
}
update()
library.on('block', update)
return () => {
stale = true
// this clears block number on network change because the library has changed
updateBlockNumber(undefined)
library.removeListener('block', update)
}
}
}, [library, updateBlockNumber])
// ...and every block...
useBlockEffect(updateBlockNumber)
}, [networkId, library, updateBlockNumber])
return null
}
export function useBetaMessageManager() {
const [state, { dismissBetaMessage }] = useApplicationContext()
return [safeAccess(state, [SHOW_BETA_MESSAGE]), dismissBetaMessage]
}
export function useBlockNumber() {
const { blockNumber } = useApplicationContext()
return blockNumber
const { networkId } = useWeb3Context()
const [state] = useApplicationContext()
return safeAccess(state, [BLOCK_NUMBERS, networkId])
}
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { safeAccess, isAddress, getEtherBalance, getTokenBalance } from '../utils'
import { useBlockNumber } from './Application'
import { useTokenDetails } from './Tokens'
const UPDATE = 'UPDATE'
const BalancesContext = createContext()
function useBalancesContext() {
return useContext(BalancesContext)
}
function reducer(state, { type, payload }) {
switch (type) {
case UPDATE: {
const { networkId, address, tokenAddress, value, blockNumber } = payload
return {
...state,
[networkId]: {
...(safeAccess(state, [networkId]) || {}),
[address]: {
...(safeAccess(state, [networkId, address]) || {}),
[tokenAddress]: {
value,
blockNumber
}
}
}
}
}
default: {
throw Error(`Unexpected action type in BalancesContext reducer: '${type}'.`)
}
}
}
export default function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, {})
const update = useCallback((networkId, address, tokenAddress, value, blockNumber) => {
dispatch({ type: UPDATE, payload: { networkId, address, tokenAddress, value, blockNumber } })
}, [])
const contextValue = useMemo(() => [state, { update }], [state, update])
return <BalancesContext.Provider value={contextValue}>{children}</BalancesContext.Provider>
}
export function useAddressBalance(address, tokenAddress) {
const { networkId, library } = useWeb3Context()
const globalBlockNumber = useBlockNumber()
const [state, { update }] = useBalancesContext()
const { value, blockNumber } = safeAccess(state, [networkId, address, tokenAddress]) || {}
useEffect(() => {
if (
isAddress(address) &&
(tokenAddress === 'ETH' || isAddress(tokenAddress)) &&
(value === undefined || blockNumber !== globalBlockNumber) &&
(networkId || networkId === 0) &&
library
) {
let stale = false
;(tokenAddress === 'ETH' ? getEtherBalance(address, library) : getTokenBalance(tokenAddress, address, library))
.then(value => {
if (!stale) {
update(networkId, address, tokenAddress, value, globalBlockNumber)
}
})
.catch(e => {
if (!stale) {
update(networkId, address, tokenAddress, null, globalBlockNumber)
}
})
return () => {
stale = true
}
}
}, [address, tokenAddress, value, blockNumber, globalBlockNumber, networkId, library, update])
return value
}
export function useExchangeReserves(tokenAddress) {
const { exchangeAddress } = useTokenDetails(tokenAddress)
const reserveETH = useAddressBalance(exchangeAddress, 'ETH')
const reserveToken = useAddressBalance(exchangeAddress, tokenAddress)
return { reserveETH, reserveToken }
}
import React, { Component, createContext, useContext, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import merge from 'lodash.merge'
import { getEtherBalance, getTokenBalance, getTokenAllowance, isAddress } from '../utils'
import { useBlockNumber } from './Application'
import { useTokenDetails } from './Static'
// define constants
const BALANCE = 'balance'
const ALLOWANCE = 'allowance'
// node creation
function createAddressValueNode(name, value, blockNumber) {
return { [name]: value, blockNumber }
}
// tree creation
function createAddressBalanceTree(address, tokenAddress, value, blockNumber) {
return { [address]: { [tokenAddress]: createAddressValueNode(BALANCE, value, blockNumber) } }
}
function createAddressAllowanceTree(address, tokenAddress, spenderAddress, value, blockNumber) {
return {
[address]: { [tokenAddress]: { [spenderAddress]: createAddressValueNode(ALLOWANCE, value, blockNumber) } }
}
}
// create contexts
const AddressBalanceContext = createContext()
const AddressAllowanceContext = createContext()
// define providers
class AddressBalanceContextProvider extends Component {
constructor(props) {
super(props)
this.getValue = (address, tokenAddress) => {
return this.state[BALANCE][address] && this.state[BALANCE][address][tokenAddress]
? this.state[BALANCE][address][tokenAddress]
: createAddressValueNode(BALANCE)
}
this.updateValue = (address, tokenAddress, value, blockNumber) => {
this.setState(state => ({
[BALANCE]: merge(state[BALANCE], createAddressBalanceTree(address, tokenAddress, value, blockNumber))
}))
}
this.clearValues = () => {
this.setState({ [BALANCE]: {} })
}
this.state = {
[BALANCE]: {},
getValue: this.getValue,
updateValue: this.updateValue,
clearValues: this.clearValues
}
}
render() {
return <AddressBalanceContext.Provider value={this.state}>{this.props.children}</AddressBalanceContext.Provider>
}
}
class AddressAllowanceContextProvider extends Component {
constructor(props) {
super(props)
this.getValue = (address, tokenAddress, spenderAddress) => {
return this.state[ALLOWANCE][address] &&
this.state[ALLOWANCE][address][tokenAddress] &&
this.state[ALLOWANCE][address][tokenAddress][spenderAddress]
? this.state[ALLOWANCE][address][tokenAddress][spenderAddress]
: createAddressValueNode(ALLOWANCE)
}
this.updateValue = (address, tokenAddress, spenderAddress, value, blockNumber) => {
this.setState(state => ({
[ALLOWANCE]: merge(
state[ALLOWANCE],
createAddressAllowanceTree(address, tokenAddress, spenderAddress, value, blockNumber)
)
}))
}
this.clearValues = () => {
this.setState({ [ALLOWANCE]: {} })
}
this.state = {
[ALLOWANCE]: {},
getValue: this.getValue,
updateValue: this.updateValue,
clearValues: this.clearValues
}
}
render() {
return <AddressAllowanceContext.Provider value={this.state}>{this.props.children}</AddressAllowanceContext.Provider>
}
}
export default function Provider({ children }) {
return (
<AddressBalanceContextProvider>
<AddressAllowanceContextProvider>{children}</AddressAllowanceContextProvider>
</AddressBalanceContextProvider>
)
}
// define useContext wrappers
function useAddressBalanceContext() {
return useContext(AddressBalanceContext)
}
function useAddressAllowanceContext() {
return useContext(AddressAllowanceContext)
}
export function Updater() {
const { networkId } = useWeb3Context()
const { clearValues: clearValuesBalance } = useAddressBalanceContext()
const { clearValues: clearValuesAllowance } = useAddressAllowanceContext()
useEffect(() => {
return () => {
clearValuesBalance()
clearValuesAllowance()
}
}, [clearValuesBalance, clearValuesAllowance, networkId])
return null
}
// define custom hooks
export function useAddressBalance(address, tokenAddress) {
const { library } = useWeb3Context()
const globalBlockNumber = useBlockNumber()
const { getValue, updateValue } = useAddressBalanceContext()
const { [BALANCE]: balance, blockNumber: balanceUpdatedBlockNumber } = getValue(address, tokenAddress)
useEffect(() => {
// gate this entire effect by checking that the inputs are valid
if (isAddress(address) && (tokenAddress === 'ETH' || isAddress(tokenAddress))) {
// if they are, and the balance is undefined or stale, fetch it
if (balance === undefined || balanceUpdatedBlockNumber !== globalBlockNumber) {
let stale = false
;(tokenAddress === 'ETH' ? getEtherBalance(address, library) : getTokenBalance(tokenAddress, address, library))
.then(value => {
if (!stale) {
updateValue(address, tokenAddress, value, globalBlockNumber)
}
})
.catch(() => {
if (!stale) {
updateValue(address, tokenAddress, null, globalBlockNumber)
}
})
return () => {
stale = true
}
}
}
}, [address, tokenAddress, balance, balanceUpdatedBlockNumber, globalBlockNumber, library, updateValue])
return balance
}
export function useAddressAllowance(address, tokenAddress, spenderAddress) {
const { library } = useWeb3Context()
const globalBlockNumber = useBlockNumber()
const { getValue, updateValue } = useAddressAllowanceContext()
const { [ALLOWANCE]: allowance, blockNumber: allowanceUpdatedBlockNumber } = getValue(
address,
tokenAddress,
spenderAddress
)
useEffect(() => {
// gate this entire effect by checking that the inputs are valid
if (isAddress(address) && isAddress(tokenAddress) && isAddress(spenderAddress)) {
// if they are, and the balance is undefined or stale, fetch it
if (allowance === undefined || allowanceUpdatedBlockNumber !== globalBlockNumber) {
let stale = false
getTokenAllowance(address, tokenAddress, spenderAddress, library)
.then(value => {
if (!stale) {
updateValue(address, tokenAddress, spenderAddress, value, globalBlockNumber)
}
})
.catch(() => {
if (!stale) {
updateValue(address, tokenAddress, spenderAddress, null, globalBlockNumber)
}
})
return () => {
stale = true
}
}
}
}, [
address,
tokenAddress,
spenderAddress,
allowance,
allowanceUpdatedBlockNumber,
globalBlockNumber,
library,
updateValue
])
return allowance
}
export function useExchangeReserves(tokenAddress) {
const { exchangeAddress } = useTokenDetails(tokenAddress)
const reserveETH = useAddressBalance(exchangeAddress, 'ETH')
const reserveToken = useAddressBalance(exchangeAddress, tokenAddress)
return { reserveETH, reserveToken }
}
This diff is collapsed.
This diff is collapsed.
import React, { Component, createContext, useContext, useCallback, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import merge from 'lodash.merge'
import { useBlockEffect } from '../hooks'
const TRANSACTION = 'transaction'
const RESPONSE = 'response'
const COMPLETED = 'completed'
const RECEIPT = 'receipt'
const TransactionContext = createContext()
function removeUndefinedValues(o) {
return Object.keys(o)
.filter(k => o[k] !== undefined)
.reduce((innerO, k) => {
innerO[k] = o[k]
return innerO
}, {})
}
function createTransactionNode(response, completed, receipt, noUndefinedValues) {
const node = { [RESPONSE]: response, [COMPLETED]: completed, [RECEIPT]: receipt }
return noUndefinedValues ? removeUndefinedValues(node) : node
}
// tree creation
function createTokenDetailTree(hash, response, completed, receipt, noUndefinedValues = false) {
return { [hash]: createTransactionNode(response, completed, receipt, noUndefinedValues) }
}
export default class Provider extends Component {
constructor(props) {
super(props)
this.getTransactions = () => {
return this.state[TRANSACTION]
}
this.addTransaction = (hash, response) => {
this.setState(state => ({
[TRANSACTION]: merge(state[TRANSACTION], createTokenDetailTree(hash, response, false))
}))
}
this.updateTransaction = (hash, receipt) => {
this.setState(state => ({
[TRANSACTION]: merge(state[TRANSACTION], createTokenDetailTree(hash, undefined, true, receipt, true))
}))
}
this.clearTransactions = () => {
this.setState({ [TRANSACTION]: {} })
}
this.state = {
[TRANSACTION]: {},
getTransactions: this.getTransactions,
addTransaction: this.addTransaction,
updateTransaction: this.updateTransaction,
clearTransactions: this.clearTransactions
}
}
render() {
return <TransactionContext.Provider value={this.state}>{this.props.children}</TransactionContext.Provider>
}
}
export function useTransactionContext() {
return useContext(TransactionContext)
}
export function Updater() {
const { library, networkId } = useWeb3Context()
const { getTransactions, updateTransaction, clearTransactions } = useTransactionContext()
useEffect(() => {
return () => {
clearTransactions()
}
}, [clearTransactions, networkId])
const updateTransactionHashes = useCallback(() => {
if (library) {
const transactions = getTransactions()
Object.keys(transactions)
.filter(k => !transactions[k][COMPLETED])
.forEach(hash => {
library.getTransactionReceipt(hash).then(receipt => {
if (receipt) {
updateTransaction(hash, receipt)
}
})
})
}
}, [library, getTransactions, updateTransaction])
useBlockEffect(updateTransactionHashes)
return null
}
export function useAllTransactions() {
const { getTransactions } = useTransactionContext()
return getTransactions()
}
export function usePendingApproval(tokenAddress) {
const allTransactions = useAllTransactions()
return (
Object.keys(allTransactions).filter(hash => {
const transaction = allTransactions[hash]
if (
transaction.completed ||
transaction.response.to !== tokenAddress ||
transaction.response.data.substring(0, 10) !== ethers.utils.id('approve(address,uint256)').substring(0, 10)
) {
return false
} else {
return true
}
}).length >= 1
)
}
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import { safeAccess } from '../utils'
import { useBlockNumber } from './Application'
const RESPONSE = 'response'
const BLOCK_NUMBER_CHECKED = 'BLOCK_NUMBER_CHECKED'
const RECEIPT = 'receipt'
const ADD = 'ADD'
const CHECK = 'CHECK'
const FINALIZE = 'FINALIZE'
const TransactionsContext = createContext()
export function useTransactionsContext() {
return useContext(TransactionsContext)
}
function reducer(state, { type, payload }) {
switch (type) {
case ADD: {
const { networkId, hash, response } = payload
if (safeAccess(state, [networkId, hash]) !== null) {
throw Error('Attempted to add existing transaction.')
}
return {
...state,
[networkId]: {
...(safeAccess(state, [networkId]) || {}),
[hash]: {
[RESPONSE]: response
}
}
}
}
case CHECK: {
const { networkId, hash, blockNumber } = payload
if (safeAccess(state, [networkId, hash]) === null) {
throw Error('Attempted to check non-existent transaction.')
}
return {
...state,
[networkId]: {
...(safeAccess(state, [networkId]) || {}),
[hash]: {
...(safeAccess(state, [networkId, hash]) || {}),
[BLOCK_NUMBER_CHECKED]: blockNumber
}
}
}
}
case FINALIZE: {
const { networkId, hash, receipt } = payload
if (safeAccess(state, [networkId, hash]) === null) {
throw Error('Attempted to finalize non-existent transaction.')
}
return {
...state,
[networkId]: {
...(safeAccess(state, [networkId]) || {}),
[hash]: {
...(safeAccess(state, [networkId, hash]) || {}),
[RECEIPT]: receipt
}
}
}
}
default: {
throw Error(`Unexpected action type in TransactionsContext reducer: '${type}'.`)
}
}
}
export default function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, {})
const add = useCallback((networkId, hash, response) => {
dispatch({ type: ADD, payload: { networkId, hash, response } })
}, [])
const check = useCallback((networkId, hash, blockNumber) => {
dispatch({ type: ADD, payload: { networkId, hash, blockNumber } })
}, [])
const finalize = useCallback((networkId, hash, receipt) => {
dispatch({ type: FINALIZE, payload: { networkId, hash, receipt } })
}, [])
const contextValue = useMemo(() => [state, { add, check, finalize }], [state, add, check, finalize])
return <TransactionsContext.Provider value={contextValue}>{children}</TransactionsContext.Provider>
}
export function Updater() {
const { networkId, library } = useWeb3Context()
const globalBlockNumber = useBlockNumber()
const [state, { check, finalize }] = useTransactionsContext()
useEffect(() => {
if ((networkId || networkId === 0) && library) {
const allTransactions = safeAccess(state, [networkId]) || {}
const allUncheckedTransactions = Object.keys(allTransactions).filter(
hash => !allTransactions[hash][RECEIPT] && allTransactions[hash][BLOCK_NUMBER_CHECKED] !== globalBlockNumber
)
let stale = false
Object.keys(allUncheckedTransactions).forEach(hash => {
library
.getTransactionReceipt(hash)
.then(receipt => {
if (!stale) {
if (!receipt) {
check(networkId, hash, globalBlockNumber)
} else {
finalize(networkId, hash, receipt)
}
}
})
.catch(() => {
check(networkId, hash, globalBlockNumber)
})
})
return () => {
stale = true
}
}
}, [networkId, library, state, globalBlockNumber, check, finalize])
return null
}
export function useTransactionAdder() {
const { networkId } = useWeb3Context()
const [, { add }] = useTransactionsContext()
return useCallback(
response => {
if (!(networkId || networkId === 0)) {
throw Error(`Invalid networkId '${networkId}`)
}
const hash = safeAccess(response, ['hash'])
if (!hash) {
throw Error('No transaction hash found.')
}
add(networkId, hash, response)
},
[networkId, add]
)
}
export function useAllTransactions() {
const { networkId } = useWeb3Context()
const [state] = useTransactionsContext()
return safeAccess(state, [networkId]) || {}
}
export function usePendingApproval(tokenAddress) {
const allTransactions = useAllTransactions()
return (
Object.keys(allTransactions).filter(hash => {
if (
allTransactions[hash][RECEIPT] ||
allTransactions[hash][RESPONSE].to !== tokenAddress ||
allTransactions[hash][RESPONSE].data.substring(0, 10) !==
ethers.utils.id('approve(address,uint256)').substring(0, 10)
) {
return false
} else {
return true
}
}).length >= 1
)
}
......@@ -23,22 +23,6 @@ export function useBodyKeyDown(targetKey, onKeyDown, suppressOnKeyDown = false)
}, [downHandler])
}
export function useBlockEffect(functionToRun) {
const { library } = useWeb3Context()
useEffect(() => {
if (library) {
function wrappedEffect(blockNumber) {
functionToRun(blockNumber)
}
library.on('block', wrappedEffect)
return () => {
library.removeListener('block', wrappedEffect)
}
}
}, [library, functionToRun])
}
// returns null on errors
export function useContract(address, ABI, withSignerIfPossible = true) {
const { library, account } = useWeb3Context()
......
......@@ -2,14 +2,16 @@ import React from 'react'
import ReactDOM from 'react-dom'
import ReactGA from 'react-ga'
import Web3Provider, { Connectors } from 'web3-react'
import ThemeProvider, { GlobalStyle } from './theme'
import ApplicationContextProvider, { Updater as ApplicationContextUpdater } from './contexts/Application'
import TransactionContextProvider, { Updater as TransactionUpdater } from './contexts/Transaction'
import StaticContextProvider, { Updater as StaticContextUpdater } from './contexts/Static'
import BlockContextProvider, { Updater as BlockContextUpdater } from './contexts/Block'
import TransactionContextProvider, { Updater as TransactionContextUpdater } from './contexts/Transactions'
import TokensContextProvider from './contexts/Tokens'
import BalancesContextProvider from './contexts/Balances'
import AllowancesContextProvider from './contexts/Allowances'
import App from './pages/App'
import './index.scss'
import './i18n'
if (process.env.NODE_ENV === 'production') {
......@@ -30,9 +32,11 @@ function ContextProviders({ children }) {
return (
<ApplicationContextProvider>
<TransactionContextProvider>
<StaticContextProvider>
<BlockContextProvider>{children}</BlockContextProvider>
</StaticContextProvider>
<TokensContextProvider>
<BalancesContextProvider>
<AllowancesContextProvider>{children}</AllowancesContextProvider>
</BalancesContextProvider>
</TokensContextProvider>
</TransactionContextProvider>
</ApplicationContextProvider>
)
......@@ -42,19 +46,22 @@ function Updaters() {
return (
<>
<ApplicationContextUpdater />
<TransactionUpdater />
<StaticContextUpdater />
<BlockContextUpdater />
<TransactionContextUpdater />
</>
)
}
ReactDOM.render(
<Web3Provider connectors={connectors} libraryName="ethers.js">
<ContextProviders>
<Updaters />
<App />
</ContextProviders>
</Web3Provider>,
<ThemeProvider>
<>
<GlobalStyle />
<Web3Provider connectors={connectors} libraryName="ethers.js">
<ContextProviders>
<Updaters />
<App />
</ContextProviders>
</Web3Provider>
</>
</ThemeProvider>,
document.getElementById('root')
)
@import url('https://rsms.me/inter/inter.css');
@import './variables.scss';
html,
body {
margin: 0;
padding: 0;
font-family: 'Inter UI', sans-serif;
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}
#root {
position: relative;
display: flex;
flex-flow: column nowrap;
height: 100vh;
width: 100vw;
overflow-x: hidden;
overflow-y: auto;
background-color: $white;
z-index: 100;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
@media only screen and (min-width: 768px) {
justify-content: center;
align-items: center;
}
}
#modal-root {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 200;
}
.loader {
border: 1px solid transparent; /* Light grey */
border-top: 1px solid $royal-blue; /* Blue */
border-radius: 50%;
width: 0.75rem;
height: 0.75rem;
margin-right: 0.25rem;
animation: spin 1s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
......@@ -10,13 +10,14 @@ import OversizedPanel from '../../components/OversizedPanel'
import ContextualInfo from '../../components/ContextualInfo'
import PlusBlue from '../../assets/images/plus-blue.svg'
import PlusGrey from '../../assets/images/plus-grey.svg'
import { useBlockEffect, useExchangeContract } from '../../hooks'
import { useExchangeContract } from '../../hooks'
import { amountFormatter, calculateGasMargin } from '../../utils'
import { useTokenDetails } from '../../contexts/Static'
import { useAddressBalance, useExchangeReserves, useAddressAllowance } from '../../contexts/Block'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useTokenDetails } from '../../contexts/Tokens'
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
import { useAddressAllowance } from '../../contexts/Allowances'
import './pool.scss'
import { useTransactionContext } from '../../contexts/Transaction'
const INPUT = 0
const OUTPUT = 1
......@@ -117,7 +118,7 @@ function getMarketRate(reserveETH, reserveToken, decimals, invert = false) {
export default function AddLiquidity() {
const { t } = useTranslation()
const { active, account } = useWeb3Context()
const { library, active, account } = useWeb3Context()
const [addLiquidityState, dispatchAddLiquidityState] = useReducer(addLiquidityStateReducer, initialAddLiquidityState)
const { inputValue, outputValue, lastEditedField, outputCurrency } = addLiquidityState
......@@ -141,8 +142,12 @@ export default function AddLiquidity() {
}, [exchangeContract])
useEffect(() => {
fetchPoolTokens()
}, [fetchPoolTokens])
useBlockEffect(fetchPoolTokens)
library.on('block', fetchPoolTokens)
return () => {
library.removeListener('block', fetchPoolTokens)
}
}, [fetchPoolTokens, library])
const poolTokenBalance = useAddressBalance(account, exchangeAddress)
const exchangeETHBalance = useAddressBalance(exchangeAddress, 'ETH')
......@@ -279,7 +284,7 @@ export default function AddLiquidity() {
)
}
const { addTransaction } = useTransactionContext()
const addTransaction = useTransactionAdder()
const isActive = active && account
const isValid = inputError === null || outputError === null
......@@ -311,7 +316,7 @@ export default function AddLiquidity() {
}
)
.then(response => {
addTransaction(response.hash, response)
addTransaction(response)
})
}
......@@ -333,7 +338,6 @@ export default function AddLiquidity() {
}
}, [decimals, inputValue, isNewExchange, outputValue])
console.log('rendering')
// parse input value
useEffect(() => {
if (isNewExchange === false && inputValue && marketRate && lastEditedField === INPUT) {
......
......@@ -8,9 +8,9 @@ import ReactGA from 'react-ga'
import AddressInputPanel from '../../components/AddressInputPanel'
import OversizedPanel from '../../components/OversizedPanel'
import { useTokenDetails } from '../../contexts/Static'
import { useTransactionContext } from '../../contexts/Transaction'
import { useFactoryContract } from '../../hooks'
import { useTokenDetails } from '../../contexts/Tokens'
import { useTransactionAdder } from '../../contexts/Transactions'
function CreateExchange({ history, location }) {
const { t } = useTranslation()
......@@ -24,7 +24,7 @@ function CreateExchange({ history, location }) {
const [tokenAddressError, setTokenAddressError] = useState()
const { name, symbol, decimals, exchangeAddress } = useTokenDetails(tokenAddress.address)
const { addTransaction } = useTransactionContext()
const addTransaction = useTransactionAdder()
// clear location state, if it exists
useEffect(() => {
......@@ -61,11 +61,12 @@ function CreateExchange({ history, location }) {
const estimatedGasLimit = await factory.estimate.createExchange(tokenAddress.address)
factory.createExchange(tokenAddress.address, { gasLimit: estimatedGasLimit }).then(response => {
addTransaction(response.hash, response)
ReactGA.event({
category: 'Pool',
action: 'CreateExchange'
})
addTransaction(response)
})
}
......
......@@ -10,11 +10,11 @@ import ContextualInfo from '../../components/ContextualInfo'
import OversizedPanel from '../../components/OversizedPanel'
import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg'
import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
import { useExchangeContract, useBlockEffect } from '../../hooks'
import { useAddressBalance } from '../../contexts/Block'
import { useTokenDetails } from '../../contexts/Static'
import { useExchangeContract } from '../../hooks'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useTokenDetails } from '../../contexts/Tokens'
import { useAddressBalance } from '../../contexts/Balances'
import { calculateGasMargin, amountFormatter } from '../../utils'
import { useTransactionContext } from '../../contexts/Transaction'
// denominated in bips
const ALLOWED_SLIPPAGE = ethers.utils.bigNumberify(200)
......@@ -71,10 +71,10 @@ function calculateSlippageBounds(value) {
}
export default function RemoveLiquidity() {
const { account, active } = useWeb3Context()
const { library, account, active } = useWeb3Context()
const { t } = useTranslation()
const { addTransaction } = useTransactionContext()
const addTransaction = useTransactionAdder()
const [outputCurrency, setOutputCurrency] = useState('')
const [value, setValue] = useState('')
......@@ -162,8 +162,12 @@ export default function RemoveLiquidity() {
}, [exchange])
useEffect(() => {
fetchPoolTokens()
}, [fetchPoolTokens])
useBlockEffect(fetchPoolTokens)
library.on('block', fetchPoolTokens)
return () => {
library.removeListener('block', fetchPoolTokens)
}
}, [fetchPoolTokens, library])
async function onRemoveLiquidity() {
ReactGA.event({
......@@ -185,7 +189,7 @@ export default function RemoveLiquidity() {
gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN)
})
.then(response => {
addTransaction(response.hash, response)
addTransaction(response)
})
}
......
......@@ -10,14 +10,14 @@ import OversizedPanel from '../../components/OversizedPanel'
import AddressInputPanel from '../../components/AddressInputPanel'
import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg'
import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
import { useAddressBalance, useAddressAllowance, useExchangeReserves } from '../../contexts/Block'
import { useTokenDetails } from '../../contexts/Static'
import { useTransactionContext } from '../../contexts/Transaction'
import { amountFormatter, calculateGasMargin } from '../../utils'
import { isAddress, amountFormatter, calculateGasMargin } from '../../utils'
import { useExchangeContract } from '../../hooks'
import { useTokenDetails } from '../../contexts/Tokens'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
import { useAddressAllowance } from '../../contexts/Allowances'
import './send.scss'
import { isAddress } from 'web3-utils'
const INPUT = 0
const OUTPUT = 1
......@@ -194,7 +194,7 @@ export default function Swap() {
const { t } = useTranslation()
const { account } = useWeb3Context()
const { addTransaction } = useTransactionContext()
const addTransaction = useTransactionAdder()
// analytics
useEffect(() => {
......@@ -619,7 +619,7 @@ export default function Swap() {
const estimatedGasLimit = await estimate(...args, { value })
method(...args, { value, gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN) }).then(response => {
addTransaction(response.hash, response)
addTransaction(response)
})
}
......
......@@ -9,11 +9,12 @@ import ContextualInfo from '../../components/ContextualInfo'
import OversizedPanel from '../../components/OversizedPanel'
import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg'
import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
import { useAddressBalance, useAddressAllowance, useExchangeReserves } from '../../contexts/Block'
import { useTokenDetails } from '../../contexts/Static'
import { useTransactionContext } from '../../contexts/Transaction'
import { amountFormatter, calculateGasMargin } from '../../utils'
import { useExchangeContract } from '../../hooks'
import { useTokenDetails } from '../../contexts/Tokens'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
import { useAddressAllowance } from '../../contexts/Allowances'
import './swap.scss'
......@@ -192,7 +193,7 @@ export default function Swap() {
const { t } = useTranslation()
const { account } = useWeb3Context()
const { addTransaction } = useTransactionContext()
const addTransaction = useTransactionAdder()
// analytics
useEffect(() => {
......@@ -284,7 +285,6 @@ export default function Swap() {
const [showUnlock, setShowUnlock] = useState(false)
useEffect(() => {
const inputValueCalculation = independentField === INPUT ? independentValueParsed : dependentValueMaximum
if (inputBalance && (inputAllowance || inputCurrency === 'ETH') && inputValueCalculation) {
if (inputBalance.lt(inputValueCalculation)) {
setInputError(t('insufficientBalance'))
......@@ -381,7 +381,6 @@ export default function Swap() {
if (intermediateValue.lte(ethers.constants.Zero)) {
throw Error()
}
// console.log('hi!', amountFormatter(intermediateValue, ))
const calculatedDependentValue = calculateEtherTokenInputFromOutput(
intermediateValue,
reserveTokenFirst,
......@@ -596,7 +595,7 @@ export default function Swap() {
const estimatedGasLimit = await estimate(...args, { value })
method(...args, { value, gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN) }).then(response => {
addTransaction(response.hash, response)
addTransaction(response)
})
}
......
import React from 'react'
import { ThemeProvider as StyledComponentsThemeProvider, createGlobalStyle } from 'styled-components'
const theme = {
uniswapPink: '#DC6BE5',
royalBlue: '#2f80ed',
white: '#FFF',
black: '#000'
}
export default function ThemeProvider({ children }) {
return <StyledComponentsThemeProvider theme={theme}>{children}</StyledComponentsThemeProvider>
}
export const GlobalStyle = createGlobalStyle`
@import url('https://rsms.me/inter/inter.css');
html,
body {
padding: 0;
margin: 0;
font-family: Inter, sans-serif;
font-variant: none;
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}
#root {
position: relative;
display: flex;
flex-flow: column nowrap;
height: 100vh;
width: 100vw;
overflow-x: hidden;
overflow-y: auto;
background-color: ${props => props.theme.white};
z-index: 100;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
@media only screen and (min-width: 768px) {
justify-content: center;
align-items: center;
}
}
#modal-root {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 200;
}
.loader {
border: 1px solid transparent;
border-top: 1px solid ${props => props.theme.royalBlue};
border-radius: 50%;
width: 0.75rem;
height: 0.75rem;
margin-right: 0.25rem;
animation: spin 1s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`
......@@ -14,6 +14,13 @@ export const ERROR_CODES = ['TOKEN_NAME', 'TOKEN_SYMBOL', 'TOKEN_DECIMALS'].redu
{}
)
export function safeAccess(object, path) {
return path.reduce(
(accumulator, currentValue) => (accumulator && accumulator[currentValue] ? accumulator[currentValue] : null),
object
)
}
export function isAddress(value) {
try {
ethers.utils.getAddress(value)
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment