Rewrite tx store (#754)

* Rewrite the transaction store

* Working state

* Fix lint errors

* Just always call getSigner
This commit is contained in:
Moody Salem 2020-05-12 18:11:10 -04:00 committed by GitHub
parent 19a53cd999
commit ef65943659
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 247 additions and 275 deletions

@ -9,7 +9,7 @@ import Copy from './Copy'
import Circle from '../../assets/images/circle.svg' import Circle from '../../assets/images/circle.svg'
import { transparentize } from 'polished' import { transparentize } from 'polished'
import { useAllTransactions } from '../../contexts/Transactions' import { useAllTransactions } from '../../state/transactions/hooks'
const TransactionStatusWrapper = styled.div` const TransactionStatusWrapper = styled.div`
display: flex; display: flex;
@ -74,7 +74,7 @@ export default function Transaction({ hash, pending }: { hash: string; pending:
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const allTransactions = useAllTransactions() const allTransactions = useAllTransactions()
const summary = allTransactions?.[hash]?.response?.summary const summary = allTransactions?.[hash]?.summary
return ( return (
<TransactionWrapper key={hash}> <TransactionWrapper key={hash}>

@ -1,3 +1,4 @@
import { JsonRpcSigner } from '@ethersproject/providers'
import React, { useState, useCallback, useEffect, useContext } from 'react' import React, { useState, useCallback, useEffect, useContext } from 'react'
import { ThemeContext } from 'styled-components' import { ThemeContext } from 'styled-components'
import { parseEther, parseUnits } from '@ethersproject/units' import { parseEther, parseUnits } from '@ethersproject/units'
@ -19,7 +20,7 @@ import { useAddressBalance, useAllBalances } from '../../contexts/Balances'
import { useAddUserToken, useFetchTokenByAddress } from '../../state/user/hooks' import { useAddUserToken, useFetchTokenByAddress } from '../../state/user/hooks'
import { usePair } from '../../data/Reserves' import { usePair } from '../../data/Reserves'
import { useAllTokens, useToken } from '../../contexts/Tokens' import { useAllTokens, useToken } from '../../contexts/Tokens'
import { usePendingApproval, useTransactionAdder } from '../../contexts/Transactions' import { useHasPendingApproval, useTransactionAdder } from '../../state/transactions/hooks'
import { useTokenContract, useWeb3React } from '../../hooks' import { useTokenContract, useWeb3React } from '../../hooks'
import { useTradeExactIn, useTradeExactOut } from '../../hooks/Trades' import { useTradeExactIn, useTradeExactOut } from '../../hooks/Trades'
import { useWalletModalToggle } from '../../state/application/hooks' import { useWalletModalToggle } from '../../state/application/hooks'
@ -28,10 +29,10 @@ import { Link } from '../../theme/components'
import { import {
calculateGasMargin, calculateGasMargin,
getEtherscanLink, getEtherscanLink,
getProviderOrSigner,
getRouterContract, getRouterContract,
QueryParams, QueryParams,
calculateSlippageAmount calculateSlippageAmount,
getSigner
} from '../../utils' } from '../../utils'
import Copy from '../AccountDetails/Copy' import Copy from '../AccountDetails/Copy'
import AddressInputPanel from '../AddressInputPanel' import AddressInputPanel from '../AddressInputPanel'
@ -210,7 +211,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
(!!inputApproval && (!!inputApproval &&
!!parsedAmounts[Field.INPUT] && !!parsedAmounts[Field.INPUT] &&
JSBI.greaterThanOrEqual(inputApproval.raw, parsedAmounts[Field.INPUT].raw)) JSBI.greaterThanOrEqual(inputApproval.raw, parsedAmounts[Field.INPUT].raw))
const pendingApprovalInput = usePendingApproval(tokens[Field.INPUT]?.address) const pendingApprovalInput = useHasPendingApproval(tokens[Field.INPUT]?.address)
const feeAsPercent = new Percent(JSBI.BigInt(3), JSBI.BigInt(1000)) const feeAsPercent = new Percent(JSBI.BigInt(3), JSBI.BigInt(1000))
const feeTimesInputRaw = const feeTimesInputRaw =
@ -370,23 +371,23 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
async function onSend() { async function onSend() {
setAttemptingTxn(true) setAttemptingTxn(true)
const signer = await getProviderOrSigner(library, account) const signer = getSigner(library, account)
// get token contract if needed // get token contract if needed
let estimate: Function, method: Function, args let estimate: Function, method: Function, args
if (tokens[Field.INPUT].equals(WETH[chainId])) { if (tokens[Field.INPUT].equals(WETH[chainId])) {
;(signer as any) signer
.sendTransaction({ to: recipient.toString(), value: BigNumber.from(parsedAmounts[Field.INPUT].raw.toString()) }) .sendTransaction({ to: recipient.toString(), value: BigNumber.from(parsedAmounts[Field.INPUT].raw.toString()) })
.then(response => { .then(response => {
setTxHash(response.hash) setTxHash(response.hash)
addTransaction( addTransaction(response, {
response, summary:
'Send ' + 'Send ' +
parsedAmounts[Field.INPUT]?.toSignificant(3) + parsedAmounts[Field.INPUT]?.toSignificant(3) +
' ' + ' ' +
tokens[Field.INPUT]?.symbol + tokens[Field.INPUT]?.symbol +
' to ' + ' to ' +
recipient recipient
) })
setPendingConfirmation(false) setPendingConfirmation(false)
}) })
.catch(() => { .catch(() => {
@ -403,15 +404,15 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
gasLimit: calculateGasMargin(estimatedGasLimit) gasLimit: calculateGasMargin(estimatedGasLimit)
}).then(response => { }).then(response => {
setTxHash(response.hash) setTxHash(response.hash)
addTransaction( addTransaction(response, {
response, summary:
'Send ' + 'Send ' +
parsedAmounts[Field.INPUT]?.toSignificant(3) + parsedAmounts[Field.INPUT]?.toSignificant(3) +
' ' + ' ' +
tokens[Field.INPUT]?.symbol + tokens[Field.INPUT]?.symbol +
' to ' + ' to ' +
recipient recipient
) })
setPendingConfirmation(false) setPendingConfirmation(false)
}) })
) )
@ -514,9 +515,9 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
gasLimit: calculateGasMargin(estimatedGasLimit) gasLimit: calculateGasMargin(estimatedGasLimit)
}).then(response => { }).then(response => {
setTxHash(response.hash) setTxHash(response.hash)
addTransaction( addTransaction(response, {
response, summary:
'Swap ' + 'Swap ' +
slippageAdjustedAmounts?.[Field.INPUT]?.toSignificant(3) + slippageAdjustedAmounts?.[Field.INPUT]?.toSignificant(3) +
' ' + ' ' +
tokens[Field.INPUT]?.symbol + tokens[Field.INPUT]?.symbol +
@ -524,7 +525,7 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
slippageAdjustedAmounts?.[Field.OUTPUT]?.toSignificant(3) + slippageAdjustedAmounts?.[Field.OUTPUT]?.toSignificant(3) +
' ' + ' ' +
tokens[Field.OUTPUT]?.symbol tokens[Field.OUTPUT]?.symbol
) })
setPendingConfirmation(false) setPendingConfirmation(false)
}) })
) )
@ -550,7 +551,10 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
gasLimit: calculateGasMargin(estimatedGas) gasLimit: calculateGasMargin(estimatedGas)
}) })
.then(response => { .then(response => {
addTransaction(response, 'Approve ' + tokens[field]?.symbol, { approval: tokens[field]?.address }) addTransaction(response, {
summary: 'Approve ' + tokens[field]?.symbol,
approvalOfToken: tokens[field].address
})
}) })
} }

@ -4,7 +4,7 @@ import { useMediaLayout } from 'use-media'
import { X } from 'react-feather' import { X } from 'react-feather'
import { PopupContent } from '../../state/application/actions' import { PopupContent } from '../../state/application/actions'
import { usePopups } from '../../state/application/hooks' import { useActivePopups, useRemovePopup } from '../../state/application/hooks'
import { Link } from '../../theme' import { Link } from '../../theme'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
import DoubleTokenLogo from '../DoubleLogo' import DoubleTokenLogo from '../DoubleLogo'
@ -102,7 +102,8 @@ function PopupItem({ content, popKey }: { content: PopupContent; popKey: string
export default function App() { export default function App() {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
// get all popups // get all popups
const [activePopups, , removePopup] = usePopups() const activePopups = useActivePopups()
const removePopup = useRemovePopup()
// switch view settings on mobile // switch view settings on mobile
const isMobile = useMediaLayout({ maxWidth: '600px' }) const isMobile = useMediaLayout({ maxWidth: '600px' })

@ -6,7 +6,7 @@ import styled from 'styled-components'
import { useWeb3React } from '../../hooks' import { useWeb3React } from '../../hooks'
import useInterval from '../../hooks/useInterval' import useInterval from '../../hooks/useInterval'
import { usePopups } from '../../state/application/hooks' import { useRemovePopup } from '../../state/application/hooks'
import { TYPE } from '../../theme' import { TYPE } from '../../theme'
import { Link } from '../../theme/components' import { Link } from '../../theme/components'
@ -41,7 +41,7 @@ export default function TxnPopup({
const [count, setCount] = useState(1) const [count, setCount] = useState(1)
const [isRunning, setIsRunning] = useState(true) const [isRunning, setIsRunning] = useState(true)
const [, , removePopup] = usePopups() const removePopup = useRemovePopup()
useInterval( useInterval(
() => { () => {

@ -20,7 +20,7 @@ import LightCircle from '../../assets/svg/lightcircle.svg'
import { RowBetween } from '../Row' import { RowBetween } from '../Row'
import { useENSName } from '../../hooks' import { useENSName } from '../../hooks'
import { shortenAddress } from '../../utils' import { shortenAddress } from '../../utils'
import { useAllTransactions } from '../../contexts/Transactions' import { useAllTransactions } from '../../state/transactions/hooks'
import { NetworkContextName } from '../../constants' import { NetworkContextName } from '../../constants'
import { injected, walletconnect, walletlink, fortmatic, portis } from '../../connectors' import { injected, walletconnect, walletlink, fortmatic, portis } from '../../connectors'

@ -1,213 +0,0 @@
import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react'
import { useWeb3React } from '../hooks'
import { useBlockNumber, usePopups } from '../state/application/hooks'
const ADD = 'ADD'
const CHECK = 'CHECK'
const FINALIZE = 'FINALIZE'
interface TransactionState {
[chainId: number]: {
[txHash: string]: {
blockNumberChecked: any
response: {
customData?: any
summary: any
}
receipt: any
}
}
}
const TransactionsContext = createContext<[TransactionState, { [updater: string]: (...args: any[]) => void }]>([{}, {}])
export function useTransactionsContext() {
return useContext(TransactionsContext)
}
function reducer(state: TransactionState, { type, payload }): TransactionState {
switch (type) {
case ADD: {
const { networkId, hash, response } = payload
if (state[networkId]?.[hash]) {
throw Error('Attempted to add existing transaction.')
}
return {
...state,
[networkId]: {
...state[networkId],
[hash]: {
response
}
}
}
}
case CHECK: {
const { networkId, hash, blockNumber } = payload
if (!state[networkId]?.[hash]) {
throw Error('Attempted to check non-existent transaction.')
}
return {
...state,
[networkId]: {
...state[networkId],
[hash]: {
...state[networkId]?.[hash],
blockNumberChecked: blockNumber
}
}
}
}
case FINALIZE: {
const { networkId, hash, receipt } = payload
if (!state[networkId]?.[hash]) {
throw Error('Attempted to finalize non-existent transaction.')
}
return {
...state,
[networkId]: {
...state[networkId],
[hash]: {
...state[networkId]?.[hash],
receipt
}
}
}
}
default: {
throw Error(`Unexpected action type in TransactionsContext reducer: '${type}'.`)
}
}
}
export default function Provider({ children }: { children: React.ReactNode }) {
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: CHECK, payload: { networkId, hash, blockNumber } })
}, [])
const finalize = useCallback((networkId, hash, receipt) => {
dispatch({ type: FINALIZE, payload: { networkId, hash, receipt } })
}, [])
return (
<TransactionsContext.Provider
value={useMemo(() => [state, { add, check, finalize }], [state, add, check, finalize])}
>
{children}
</TransactionsContext.Provider>
)
}
export function Updater() {
const { chainId, library } = useWeb3React()
const globalBlockNumber = useBlockNumber()
const [state, { check, finalize }] = useTransactionsContext()
const allTransactions = state[chainId] ?? {}
// show popup on confirm
const [, addPopup] = usePopups()
useEffect(() => {
if ((chainId || chainId === 0) && library) {
let stale = false
Object.keys(allTransactions)
.filter(
hash => !allTransactions[hash].receipt && allTransactions[hash].blockNumberChecked !== globalBlockNumber
)
.forEach(hash => {
library
.getTransactionReceipt(hash)
.then(receipt => {
if (!stale) {
if (!receipt) {
check(chainId, hash, globalBlockNumber)
} else {
finalize(chainId, hash, receipt)
// add success or failure popup
if (receipt.status === 1) {
addPopup({
txn: {
hash,
success: true,
summary: allTransactions[hash]?.response?.summary
}
})
} else {
addPopup({
txn: { hash, success: false, summary: allTransactions[hash]?.response?.summary }
})
}
}
}
})
.catch(() => {
check(chainId, hash, globalBlockNumber)
})
})
return () => {
stale = true
}
}
}, [chainId, library, allTransactions, globalBlockNumber, check, finalize, addPopup])
return null
}
export function useTransactionAdder() {
const { chainId } = useWeb3React()
const [, { add }] = useTransactionsContext()
return useCallback(
(response, summary = '', customData = {}) => {
if (!(chainId || chainId === 0)) {
throw Error(`Invalid networkId '${chainId}`)
}
const hash = response?.hash
if (!hash) {
throw Error('No transaction hash found.')
}
add(chainId, hash, { ...response, customData: customData, summary })
},
[chainId, add]
)
}
export function useAllTransactions() {
const { chainId } = useWeb3React()
const [state] = useTransactionsContext()
return state[chainId] || {}
}
export function usePendingApproval(tokenAddress) {
const allTransactions = useAllTransactions()
return (
Object.keys(allTransactions).filter(hash => {
if (allTransactions[hash]?.receipt) {
return false
} else if (!allTransactions[hash]?.response) {
return false
} else if (allTransactions[hash]?.response?.customData?.approval !== tokenAddress) {
return false
} else {
return true
}
}).length >= 1
)
}

@ -8,7 +8,7 @@ import { Provider } from 'react-redux'
import { NetworkContextName } from './constants' import { NetworkContextName } from './constants'
import { isMobile } from 'react-device-detect' import { isMobile } from 'react-device-detect'
import { Updater as LocalStorageContextUpdater } from './state/user/hooks' import { Updater as LocalStorageContextUpdater } from './state/user/hooks'
import TransactionContextProvider, { Updater as TransactionContextUpdater } from './contexts/Transactions' import { Updater as TransactionContextUpdater } from './state/transactions/hooks'
import BalancesContextProvider, { Updater as BalancesContextUpdater } from './contexts/Balances' import BalancesContextProvider, { Updater as BalancesContextUpdater } from './contexts/Balances'
import App from './pages/App' import App from './pages/App'
import store from './state' import store from './state'
@ -36,11 +36,7 @@ if (process.env.NODE_ENV === 'production') {
ReactGA.pageview(window.location.pathname + window.location.search) ReactGA.pageview(window.location.pathname + window.location.search)
function ContextProviders({ children }: { children: React.ReactNode }) { function ContextProviders({ children }: { children: React.ReactNode }) {
return ( return <BalancesContextProvider>{children}</BalancesContextProvider>
<TransactionContextProvider>
<BalancesContextProvider>{children}</BalancesContextProvider>
</TransactionContextProvider>
)
} }
function Updaters() { function Updaters() {

@ -25,7 +25,7 @@ import { useAddressBalance } from '../../contexts/Balances'
import { useTokenAllowance } from '../../data/Allowances' import { useTokenAllowance } from '../../data/Allowances'
import { useTotalSupply } from '../../data/TotalSupply' import { useTotalSupply } from '../../data/TotalSupply'
import { useWeb3React, useTokenContract } from '../../hooks' import { useWeb3React, useTokenContract } from '../../hooks'
import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions' import { useTransactionAdder, useHasPendingApproval } from '../../state/transactions/hooks'
import { ROUTER_ADDRESS } from '../../constants' import { ROUTER_ADDRESS } from '../../constants'
import { getRouterContract, calculateGasMargin, calculateSlippageAmount } from '../../utils' import { getRouterContract, calculateGasMargin, calculateSlippageAmount } from '../../utils'
@ -305,8 +305,8 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
!!parsedAmounts[Field.OUTPUT] && !!parsedAmounts[Field.OUTPUT] &&
JSBI.greaterThanOrEqual(outputApproval.raw, parsedAmounts[Field.OUTPUT].raw)) JSBI.greaterThanOrEqual(outputApproval.raw, parsedAmounts[Field.OUTPUT].raw))
// check on pending approvals for token amounts // check on pending approvals for token amounts
const pendingApprovalInput = usePendingApproval(tokens[Field.INPUT]?.address) const pendingApprovalInput = useHasPendingApproval(tokens[Field.INPUT]?.address)
const pendingApprovalOutput = usePendingApproval(tokens[Field.OUTPUT]?.address) const pendingApprovalOutput = useHasPendingApproval(tokens[Field.OUTPUT]?.address)
// used for displaying approximate starting price in UI // used for displaying approximate starting price in UI
const derivedPrice = const derivedPrice =
@ -497,9 +497,9 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
gasLimit: calculateGasMargin(estimatedGasLimit) gasLimit: calculateGasMargin(estimatedGasLimit)
}).then(response => { }).then(response => {
setTxHash(response.hash) setTxHash(response.hash)
addTransaction( addTransaction(response, {
response, summary:
'Add ' + 'Add ' +
parsedAmounts[Field.INPUT]?.toSignificant(3) + parsedAmounts[Field.INPUT]?.toSignificant(3) +
' ' + ' ' +
tokens[Field.INPUT]?.symbol + tokens[Field.INPUT]?.symbol +
@ -507,7 +507,7 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
parsedAmounts[Field.OUTPUT]?.toSignificant(3) + parsedAmounts[Field.OUTPUT]?.toSignificant(3) +
' ' + ' ' +
tokens[Field.OUTPUT]?.symbol tokens[Field.OUTPUT]?.symbol
) })
setPendingConfirmation(false) setPendingConfirmation(false)
}) })
) )
@ -534,7 +534,10 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
gasLimit: calculateGasMargin(estimatedGas) gasLimit: calculateGasMargin(estimatedGas)
}) })
.then(response => { .then(response => {
addTransaction(response, 'Approve ' + tokens[field]?.symbol, { approval: tokens[field]?.address }) addTransaction(response, {
summary: 'Approve ' + tokens[field]?.symbol,
approvalOfToken: tokens[field].address
})
}) })
} }

@ -23,7 +23,7 @@ import { useToken } from '../../contexts/Tokens'
import { useWeb3React } from '../../hooks' import { useWeb3React } from '../../hooks'
import { useAllBalances } from '../../contexts/Balances' import { useAllBalances } from '../../contexts/Balances'
import { usePairContract } from '../../hooks' import { usePairContract } from '../../hooks'
import { useTransactionAdder } from '../../contexts/Transactions' import { useTransactionAdder } from '../../state/transactions/hooks'
import { useTotalSupply } from '../../data/TotalSupply' import { useTotalSupply } from '../../data/TotalSupply'
import { splitSignature } from '@ethersproject/bytes' import { splitSignature } from '@ethersproject/bytes'
@ -493,9 +493,9 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
}).then(response => { }).then(response => {
setPendingConfirmation(false) setPendingConfirmation(false)
setTxHash(response.hash) setTxHash(response.hash)
addTransaction( addTransaction(response, {
response, summary:
'Remove ' + 'Remove ' +
parsedAmounts[Field.TOKEN0]?.toSignificant(3) + parsedAmounts[Field.TOKEN0]?.toSignificant(3) +
' ' + ' ' +
tokens[Field.TOKEN0]?.symbol + tokens[Field.TOKEN0]?.symbol +
@ -503,7 +503,7 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
parsedAmounts[Field.TOKEN1]?.toSignificant(3) + parsedAmounts[Field.TOKEN1]?.toSignificant(3) +
' ' + ' ' +
tokens[Field.TOKEN1]?.symbol tokens[Field.TOKEN1]?.symbol
) })
}) })
) )
.catch(e => { .catch(e => {

@ -1,4 +1,4 @@
import { useCallback } from 'react' import { useCallback, useMemo } from 'react'
import { useWeb3React } from '../../hooks' import { useWeb3React } from '../../hooks'
import { addPopup, PopupContent, removePopup, toggleWalletModal } from './actions' import { addPopup, PopupContent, removePopup, toggleWalletModal } from './actions'
import { useSelector, useDispatch } from 'react-redux' import { useSelector, useDispatch } from 'react-redux'
@ -23,27 +23,31 @@ export function useUserAdvanced() {
return useSelector((state: AppState) => state.application.userAdvanced) return useSelector((state: AppState) => state.application.userAdvanced)
} }
export function usePopups(): [ // returns a function that allows adding a popup
AppState['application']['popupList'], export function useAddPopup(): (content: PopupContent) => void {
(content: PopupContent) => void,
(key: string) => void
] {
const dispatch = useDispatch() const dispatch = useDispatch()
const activePopups = useSelector((state: AppState) => state.application.popupList.filter(item => item.show))
const wrappedAddPopup = useCallback( return useCallback(
(content: PopupContent) => { (content: PopupContent) => {
dispatch(addPopup({ content })) dispatch(addPopup({ content }))
}, },
[dispatch] [dispatch]
) )
}
const wrappedRemovePopup = useCallback( // returns a function that allows removing a popup via its key
export function useRemovePopup(): (key: string) => void {
const dispatch = useDispatch()
return useCallback(
(key: string) => { (key: string) => {
dispatch(removePopup({ key })) dispatch(removePopup({ key }))
}, },
[dispatch] [dispatch]
) )
}
return [activePopups, wrappedAddPopup, wrappedRemovePopup]
// get the list of active popups
export function useActivePopups(): AppState['application']['popupList'] {
const list = useSelector((state: AppState) => state.application.popupList)
return useMemo(() => list.filter(item => item.show), [list])
} }

@ -1,14 +1,16 @@
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit' import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
import application from './application/reducer' import application from './application/reducer'
import user from './user/reducer' import user from './user/reducer'
import transactions from './transactions/reducer'
import { save, load } from 'redux-localstorage-simple' import { save, load } from 'redux-localstorage-simple'
const PERSISTED_KEYS: string[] = ['user'] const PERSISTED_KEYS: string[] = ['user', 'transactions']
const store = configureStore({ const store = configureStore({
reducer: { reducer: {
application, application,
user user,
transactions
}, },
middleware: [...getDefaultMiddleware(), save({ states: PERSISTED_KEYS })], middleware: [...getDefaultMiddleware(), save({ states: PERSISTED_KEYS })],
preloadedState: load({ states: PERSISTED_KEYS }) preloadedState: load({ states: PERSISTED_KEYS })

@ -0,0 +1,13 @@
import { TransactionReceipt } from '@ethersproject/providers'
import { createAction } from '@reduxjs/toolkit'
export const addTransaction = createAction<{
chainId: number
hash: string
approvalOfToken?: string
summary?: string
}>('addTransaction')
export const checkTransaction = createAction<{ chainId: number; hash: string; blockNumber: number }>('checkTransaction')
export const finalizeTransaction = createAction<{ chainId: number; hash: string; receipt: TransactionReceipt }>(
'finalizeTransaction'
)

@ -0,0 +1,113 @@
import { TransactionResponse } from '@ethersproject/providers'
import { useCallback, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useWeb3React } from '../../hooks'
import { useAddPopup, useBlockNumber } from '../application/hooks'
import { AppDispatch, AppState } from '../index'
import { addTransaction, checkTransaction, finalizeTransaction } from './actions'
import { TransactionDetails } from './reducer'
export function Updater() {
const { chainId, library } = useWeb3React()
const globalBlockNumber = useBlockNumber()
const dispatch = useDispatch<AppDispatch>()
const transactions = useSelector<AppState>(state => state.transactions)
const allTransactions = transactions[chainId] ?? {}
// show popup on confirm
const addPopup = useAddPopup()
useEffect(() => {
if ((chainId || chainId === 0) && library) {
let stale = false
Object.keys(allTransactions)
.filter(
hash => !allTransactions[hash].receipt && allTransactions[hash].blockNumberChecked !== globalBlockNumber
)
.forEach(hash => {
library
.getTransactionReceipt(hash)
.then(receipt => {
if (!stale) {
if (!receipt) {
dispatch(checkTransaction({ chainId, hash, blockNumber: globalBlockNumber }))
} else {
dispatch(finalizeTransaction({ chainId, hash, receipt }))
// add success or failure popup
if (receipt.status === 1) {
addPopup({
txn: {
hash,
success: true,
summary: allTransactions[hash]?.response?.summary
}
})
} else {
addPopup({
txn: { hash, success: false, summary: allTransactions[hash]?.response?.summary }
})
}
}
}
})
.catch(() => {
dispatch(checkTransaction({ chainId, hash, blockNumber: globalBlockNumber }))
})
})
return () => {
stale = true
}
}
}, [chainId, library, allTransactions, globalBlockNumber, dispatch, addPopup])
return null
}
// helper that can take a ethers library transaction response and add it to the list of transactions
export function useTransactionAdder(): (
response: TransactionResponse,
customData?: { summary?: string; approvalOfToken?: string }
) => void {
const { chainId } = useWeb3React()
const dispatch = useDispatch<AppDispatch>()
return useCallback(
(
response: TransactionResponse,
{ summary, approvalOfToken }: { summary?: string; approvalOfToken?: string } = {}
) => {
const { hash } = response
if (!hash) {
throw Error('No transaction hash found.')
}
dispatch(addTransaction({ hash, chainId, approvalOfToken, summary }))
},
[dispatch, chainId]
)
}
// returns all the transactions for the current chain
export function useAllTransactions(): { [txHash: string]: TransactionDetails } {
const { chainId } = useWeb3React()
const state = useSelector<AppState>(state => state.transactions)
return state[chainId] ?? {}
}
// returns whether a token has a pending approval transaction
export function useHasPendingApproval(tokenAddress: string): boolean {
const allTransactions = useAllTransactions()
return Object.keys(allTransactions).some(hash => {
if (allTransactions[hash]?.receipt) {
return false
} else {
return allTransactions[hash]?.approvalOfToken === tokenAddress
}
})
}

@ -0,0 +1,43 @@
import { TransactionReceipt } from '@ethersproject/providers'
import { createReducer } from '@reduxjs/toolkit'
import { addTransaction, checkTransaction, finalizeTransaction } from './actions'
export interface TransactionDetails {
approvalOfToken?: string
blockNumberChecked?: number
summary?: string
receipt?: TransactionReceipt
}
export interface TransactionState {
[chainId: number]: {
[txHash: string]: TransactionDetails
}
}
const initialState: TransactionState = {}
export default createReducer(initialState, builder =>
builder
.addCase(addTransaction, (state, { payload: { chainId, hash, approvalOfToken, summary } }) => {
if (state[chainId]?.[hash]) {
throw Error('Attempted to add existing transaction.')
}
state[chainId] = state[chainId] ?? {}
state[chainId][hash] = { approvalOfToken, summary }
})
.addCase(checkTransaction, (state, { payload: { chainId, blockNumber, hash } }) => {
if (!state[chainId]?.[hash]) {
throw Error('Attempted to check non-existent transaction.')
}
state[chainId][hash].blockNumberChecked = blockNumber
})
.addCase(finalizeTransaction, (state, { payload: { hash, chainId, receipt } }) => {
if (!state[chainId]?.[hash]) {
throw Error('Attempted to finalize non-existent transaction.')
}
state[chainId] = state[chainId] ?? {}
state[chainId][hash].receipt = receipt
})
)

@ -1,6 +1,7 @@
import { Contract } from '@ethersproject/contracts' import { Contract } from '@ethersproject/contracts'
import { getAddress } from '@ethersproject/address' import { getAddress } from '@ethersproject/address'
import { AddressZero } from '@ethersproject/constants' import { AddressZero } from '@ethersproject/constants'
import { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
import { parseBytes32String } from '@ethersproject/strings' import { parseBytes32String } from '@ethersproject/strings'
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
@ -116,13 +117,18 @@ export function calculateSlippageAmount(value: TokenAmount, slippage: number): [
] ]
} }
// account is optional // account is not optional
export function getProviderOrSigner(library: any, account?: string): any { export function getSigner(library: Web3Provider, account: string): JsonRpcSigner {
return account ? library.getSigner(account).connectUnchecked() : library return library.getSigner(account).connectUnchecked()
} }
// account is optional // account is optional
export function getContract(address: string, ABI: any, library: any, account?: string): Contract { export function getProviderOrSigner(library: Web3Provider, account?: string): Web3Provider | JsonRpcSigner {
return account ? getSigner(library, account) : library
}
// account is optional
export function getContract(address: string, ABI: any, library: Web3Provider, account?: string): Contract {
if (!isAddress(address) || address === AddressZero) { if (!isAddress(address) || address === AddressZero) {
throw Error(`Invalid 'address' parameter '${address}'.`) throw Error(`Invalid 'address' parameter '${address}'.`)
} }