Commit 5405648a authored by Moody Salem's avatar Moody Salem Committed by GitHub

Improve the transaction render style (#767)

parent 4ba7dd95
import React from 'react' import React from 'react'
import styled, { keyframes } from 'styled-components' import styled from 'styled-components'
import { Check, Triangle } from 'react-feather' import { Check, Triangle } from 'react-feather'
import { useWeb3React } from '../../hooks' import { useWeb3React } from '../../hooks'
import { getEtherscanLink } from '../../utils' import { getEtherscanLink } from '../../utils'
import { Link, Spinner } from '../../theme' import { Link, Spinner } from '../../theme'
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 '../../state/transactions/hooks' import { useAllTransactions } from '../../state/transactions/hooks'
const TransactionStatusWrapper = styled.div`
display: flex;
align-items: center;
min-width: 12px;
word-break: break-word;
`
const TransactionWrapper = styled.div` const TransactionWrapper = styled.div`
${({ theme }) => theme.flexRowNoWrap}
justify-content: space-between;
width: 100%;
margin-top: 0.75rem; margin-top: 0.75rem;
a {
min-width: 0;
word-break: break-word;
}
` `
const TransactionStatusText = styled.span` const TransactionStatusText = styled.div`
margin-left: 0.5rem; margin-right: 0.5rem;
word-break: keep-all;
` `
const rotate = keyframes` const TransactionState = styled(Link)<{ pending: boolean; success?: boolean }>`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
const TransactionState = styled.div<{ pending: boolean; success?: boolean }>`
display: flex; display: flex;
background-color: ${({ pending, success, theme }) => justify-content: space-between;
pending text-decoration: none !important;
? transparentize(0.95, theme.primary1)
: success border-radius: 0.5rem;
? transparentize(0.95, theme.green1)
: transparentize(0.95, theme.red1)};
border-radius: 1.5rem;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
font-weight: 500; font-weight: 500;
font-size: 0.75rem; font-size: 0.75rem;
border: 1px solid; border: 1px solid;
color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)};
border-color: ${({ pending, success, theme }) => border-color: ${({ pending, success, theme }) =>
pending pending
? transparentize(0.75, theme.primary1) ? transparentize(0.75, theme.primary1)
...@@ -63,10 +38,6 @@ const TransactionState = styled.div<{ pending: boolean; success?: boolean }>` ...@@ -63,10 +38,6 @@ const TransactionState = styled.div<{ pending: boolean; success?: boolean }>`
? transparentize(0.75, theme.green1) ? transparentize(0.75, theme.green1)
: transparentize(0.75, theme.red1)}; : transparentize(0.75, theme.red1)};
#pending {
animation: 2s ${rotate} linear infinite;
}
:hover { :hover {
border-color: ${({ pending, success, theme }) => border-color: ${({ pending, success, theme }) =>
pending pending
...@@ -76,11 +47,6 @@ const TransactionState = styled.div<{ pending: boolean; success?: boolean }>` ...@@ -76,11 +47,6 @@ const TransactionState = styled.div<{ pending: boolean; success?: boolean }>`
: transparentize(0, theme.red1)}; : transparentize(0, theme.red1)};
} }
` `
const ButtonWrapper = styled.div<{ pending: boolean; success?: boolean }>`
a {
color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)};
}
`
export default function Transaction({ hash }: { hash: string }) { export default function Transaction({ hash }: { hash: string }) {
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
...@@ -93,19 +59,11 @@ export default function Transaction({ hash }: { hash: string }) { ...@@ -93,19 +59,11 @@ export default function Transaction({ hash }: { hash: string }) {
(allTransactions[hash].receipt.status === 1 || typeof allTransactions[hash].receipt.status === 'undefined') (allTransactions[hash].receipt.status === 1 || typeof allTransactions[hash].receipt.status === 'undefined')
return ( return (
<TransactionWrapper key={hash}> <TransactionWrapper>
<TransactionStatusWrapper> <TransactionState href={getEtherscanLink(chainId, hash, 'transaction')} pending={pending} success={success}>
<Link href={getEtherscanLink(chainId, hash, 'transaction')}>{summary ? summary : hash}</Link> <TransactionStatusText>{summary ? summary : hash}</TransactionStatusText>
<Copy toCopy={hash} />
</TransactionStatusWrapper>
<ButtonWrapper pending={false} success={success}>
<Link href={getEtherscanLink(chainId, hash, 'transaction')}>
<TransactionState pending={pending} success={success}>
{pending ? <Spinner src={Circle} /> : success ? <Check size="16" /> : <Triangle size="16" />} {pending ? <Spinner src={Circle} /> : success ? <Check size="16" /> : <Triangle size="16" />}
<TransactionStatusText>{pending ? 'Pending' : success ? 'Success' : 'Failed'}</TransactionStatusText>
</TransactionState> </TransactionState>
</Link>
</ButtonWrapper>
</TransactionWrapper> </TransactionWrapper>
) )
} }
import React, { useState } from 'react' import React, { useCallback, useState } from 'react'
import { AlertCircle, CheckCircle } from 'react-feather' import { AlertCircle, CheckCircle } from 'react-feather'
...@@ -43,10 +43,11 @@ export default function TxnPopup({ ...@@ -43,10 +43,11 @@ export default function TxnPopup({
const [isRunning, setIsRunning] = useState(true) const [isRunning, setIsRunning] = useState(true)
const removePopup = useRemovePopup() const removePopup = useRemovePopup()
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
useInterval( useInterval(
() => { () => {
count > 150 && removePopup(popKey) count > 150 ? removeThisPopup() : setCount(count + 1)
setCount(count + 1)
}, },
isRunning ? delay : null isRunning ? delay : null
) )
......
...@@ -95,7 +95,8 @@ const Web3StatusConnected = styled(Web3StatusGeneric)<{ pending?: boolean }>` ...@@ -95,7 +95,8 @@ const Web3StatusConnected = styled(Web3StatusGeneric)<{ pending?: boolean }>`
border: 1px solid ${({ pending, theme }) => (pending ? theme.primary1 : theme.bg3)}; border: 1px solid ${({ pending, theme }) => (pending ? theme.primary1 : theme.bg3)};
color: ${({ pending, theme }) => (pending ? theme.white : theme.text1)}; color: ${({ pending, theme }) => (pending ? theme.white : theme.text1)};
font-weight: 500; font-weight: 500;
:hover { :hover,
:focus {
background-color: ${({ pending, theme }) => (pending ? darken(0.05, theme.primary1) : lighten(0.05, theme.bg2))}; background-color: ${({ pending, theme }) => (pending ? darken(0.05, theme.primary1) : lighten(0.05, theme.bg2))};
:focus { :focus {
......
...@@ -14,6 +14,7 @@ export interface SerializableTransactionReceipt { ...@@ -14,6 +14,7 @@ export interface SerializableTransactionReceipt {
export const addTransaction = createAction<{ export const addTransaction = createAction<{
chainId: number chainId: number
hash: string hash: string
from: string
approvalOfToken?: string approvalOfToken?: string
summary?: string summary?: string
}>('addTransaction') }>('addTransaction')
...@@ -23,3 +24,7 @@ export const finalizeTransaction = createAction<{ ...@@ -23,3 +24,7 @@ export const finalizeTransaction = createAction<{
hash: string hash: string
receipt: SerializableTransactionReceipt receipt: SerializableTransactionReceipt
}>('finalizeTransaction') }>('finalizeTransaction')
export const updateTransactionCount = createAction<{ address: string; transactionCount: number; chainId: number }>(
'updateTransactionCount'
)
...@@ -12,7 +12,7 @@ export function useTransactionAdder(): ( ...@@ -12,7 +12,7 @@ export function useTransactionAdder(): (
response: TransactionResponse, response: TransactionResponse,
customData?: { summary?: string; approvalOfToken?: string } customData?: { summary?: string; approvalOfToken?: string }
) => void { ) => void {
const { chainId } = useWeb3React() const { chainId, account } = useWeb3React()
const dispatch = useDispatch<AppDispatch>() const dispatch = useDispatch<AppDispatch>()
return useCallback( return useCallback(
...@@ -24,9 +24,9 @@ export function useTransactionAdder(): ( ...@@ -24,9 +24,9 @@ export function useTransactionAdder(): (
if (!hash) { if (!hash) {
throw Error('No transaction hash found.') throw Error('No transaction hash found.')
} }
dispatch(addTransaction({ hash, chainId, approvalOfToken, summary })) dispatch(addTransaction({ hash, from: account, chainId, approvalOfToken, summary }))
}, },
[dispatch, chainId] [dispatch, chainId, account]
) )
} }
......
import { createReducer } from '@reduxjs/toolkit' import { createReducer } from '@reduxjs/toolkit'
import { addTransaction, checkTransaction, finalizeTransaction, SerializableTransactionReceipt } from './actions' import { isAddress } from '../../utils'
import {
addTransaction,
checkTransaction,
finalizeTransaction,
SerializableTransactionReceipt,
updateTransactionCount
} from './actions'
const now = () => new Date().getTime() const now = () => new Date().getTime()
...@@ -11,6 +18,11 @@ export interface TransactionDetails { ...@@ -11,6 +18,11 @@ export interface TransactionDetails {
receipt?: SerializableTransactionReceipt receipt?: SerializableTransactionReceipt
addedTime: number addedTime: number
confirmedTime?: number confirmedTime?: number
from: string
nonce?: number // todo: find a way to populate this
// set to true when we receive a transaction count that exceeds the nonce of this transaction
unknownStatus?: boolean
} }
export interface TransactionState { export interface TransactionState {
...@@ -23,19 +35,19 @@ const initialState: TransactionState = {} ...@@ -23,19 +35,19 @@ const initialState: TransactionState = {}
export default createReducer(initialState, builder => export default createReducer(initialState, builder =>
builder builder
.addCase(addTransaction, (state, { payload: { chainId, hash, approvalOfToken, summary } }) => { .addCase(addTransaction, (state, { payload: { chainId, from, hash, approvalOfToken, summary } }) => {
if (state[chainId]?.[hash]) { if (state[chainId]?.[hash]) {
throw Error('Attempted to add existing transaction.') throw Error('Attempted to add existing transaction.')
} }
state[chainId] = state[chainId] ?? {} state[chainId] = state[chainId] ?? {}
state[chainId][hash] = { hash, approvalOfToken, summary, addedTime: now() } state[chainId][hash] = { hash, approvalOfToken, summary, from, addedTime: now() }
}) })
.addCase(checkTransaction, (state, { payload: { chainId, blockNumber, hash } }) => { .addCase(checkTransaction, (state, { payload: { chainId, blockNumber, hash } }) => {
if (!state[chainId]?.[hash]) { if (!state[chainId]?.[hash]) {
throw Error('Attempted to check non-existent transaction.') throw Error('Attempted to check non-existent transaction.')
} }
state[chainId][hash].blockNumberChecked = blockNumber state[chainId][hash].blockNumberChecked = Math.max(blockNumber ?? 0, state[chainId][hash].blockNumberChecked ?? 0)
}) })
.addCase(finalizeTransaction, (state, { payload: { hash, chainId, receipt } }) => { .addCase(finalizeTransaction, (state, { payload: { hash, chainId, receipt } }) => {
if (!state[chainId]?.[hash]) { if (!state[chainId]?.[hash]) {
...@@ -43,6 +55,17 @@ export default createReducer(initialState, builder => ...@@ -43,6 +55,17 @@ export default createReducer(initialState, builder =>
} }
state[chainId] = state[chainId] ?? {} state[chainId] = state[chainId] ?? {}
state[chainId][hash].receipt = receipt state[chainId][hash].receipt = receipt
state[chainId][hash].unknownStatus = false
state[chainId][hash].confirmedTime = now() state[chainId][hash].confirmedTime = now()
}) })
// marks every transaction with a nonce less than the transaction count unknown if it was pending
// this can be overridden by a finalize that comes later
.addCase(updateTransactionCount, (state, { payload: { transactionCount, address, chainId } }) => {
// mark any transactions under the transaction count to be unknown status
Object.values(state?.[chainId] ?? {})
.filter(t => !t.receipt)
.filter(t => t.from === isAddress(address))
.filter(t => typeof t.nonce && t.nonce < transactionCount)
.forEach(t => (t.unknownStatus = t.unknownStatus ?? true))
})
) )
...@@ -3,12 +3,13 @@ import { useDispatch, useSelector } from 'react-redux' ...@@ -3,12 +3,13 @@ import { useDispatch, useSelector } from 'react-redux'
import { useWeb3React } from '../../hooks' import { useWeb3React } from '../../hooks'
import { useAddPopup, useBlockNumber } from '../application/hooks' import { useAddPopup, useBlockNumber } from '../application/hooks'
import { AppDispatch, AppState } from '../index' import { AppDispatch, AppState } from '../index'
import { checkTransaction, finalizeTransaction } from './actions' import { checkTransaction, finalizeTransaction, updateTransactionCount } from './actions'
import useSWR from 'swr'
export default function Updater() { export default function Updater() {
const { chainId, library } = useWeb3React() const { chainId, account, library } = useWeb3React()
const globalBlockNumber = useBlockNumber() const lastBlockNumber = useBlockNumber()
const dispatch = useDispatch<AppDispatch>() const dispatch = useDispatch<AppDispatch>()
const transactions = useSelector<AppState>(state => state.transactions) const transactions = useSelector<AppState>(state => state.transactions)
...@@ -18,20 +19,30 @@ export default function Updater() { ...@@ -18,20 +19,30 @@ export default function Updater() {
// show popup on confirm // show popup on confirm
const addPopup = useAddPopup() const addPopup = useAddPopup()
const { data: transactionCount } = useSWR<number | null>(['accountNonce', account, lastBlockNumber], () => {
if (!account) return null
return library.getTransactionCount(account, 'latest')
})
useEffect(() => {
if (transactionCount === null) return
dispatch(updateTransactionCount({ address: account, transactionCount, chainId }))
}, [transactionCount, account, chainId, dispatch])
useEffect(() => { useEffect(() => {
if ((chainId || chainId === 0) && library) { if (typeof chainId === 'number' && library) {
let stale = false
Object.keys(allTransactions) Object.keys(allTransactions)
.filter(hash => !allTransactions[hash].receipt)
.filter( .filter(
hash => !allTransactions[hash].receipt && allTransactions[hash].blockNumberChecked !== globalBlockNumber hash =>
!allTransactions[hash].blockNumberChecked || allTransactions[hash].blockNumberChecked < lastBlockNumber
) )
.forEach(hash => { .forEach(hash => {
library library
.getTransactionReceipt(hash) .getTransactionReceipt(hash)
.then(receipt => { .then(receipt => {
if (!stale) {
if (!receipt) { if (!receipt) {
dispatch(checkTransaction({ chainId, hash, blockNumber: globalBlockNumber })) dispatch(checkTransaction({ chainId, hash, blockNumber: lastBlockNumber }))
} else { } else {
dispatch( dispatch(
finalizeTransaction({ finalizeTransaction({
...@@ -64,18 +75,13 @@ export default function Updater() { ...@@ -64,18 +75,13 @@ export default function Updater() {
}) })
} }
} }
}
}) })
.catch(() => { .catch(error => {
dispatch(checkTransaction({ chainId, hash, blockNumber: globalBlockNumber })) console.error(`failed to check transaction hash: ${hash}`, error)
}) })
}) })
return () => {
stale = true
}
} }
}, [chainId, library, allTransactions, globalBlockNumber, dispatch, addPopup]) }, [chainId, library, allTransactions, lastBlockNumber, dispatch, addPopup])
return null return null
} }
import { getAddress } from '@ethersproject/address'
import { JSBI, Token, TokenAmount, WETH } from '@uniswap/sdk' import { JSBI, Token, TokenAmount, WETH } from '@uniswap/sdk'
import { useEffect, useMemo } from 'react' import { useEffect, useMemo } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { useAllTokens } from '../../contexts/Tokens' import { useAllTokens } from '../../contexts/Tokens'
import { useWeb3React } from '../../hooks' import { usePrevious, useWeb3React } from '../../hooks'
import { isAddress } from '../../utils' import { isAddress } from '../../utils'
import { AppDispatch, AppState } from '../index' import { AppDispatch, AppState } from '../index'
import { import {
...@@ -21,16 +22,29 @@ export function useETHBalances(uncheckedAddresses?: (string | undefined)[]): { [ ...@@ -21,16 +22,29 @@ export function useETHBalances(uncheckedAddresses?: (string | undefined)[]): { [
const dispatch = useDispatch<AppDispatch>() const dispatch = useDispatch<AppDispatch>()
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const addresses: string[] = useMemo(() => (uncheckedAddresses ? uncheckedAddresses.filter(isAddress) : []), [ const addresses: string[] = useMemo(
() =>
uncheckedAddresses uncheckedAddresses
]) ? uncheckedAddresses
.filter(isAddress)
.map(getAddress)
.sort()
: [],
[uncheckedAddresses]
)
const previousAddresses = usePrevious(addresses)
const unchanged = JSON.stringify(previousAddresses) === JSON.stringify(addresses)
// add the listeners on mount, remove them on dismount // add the listeners on mount, remove them on dismount
useEffect(() => { useEffect(() => {
if (unchanged) return
if (addresses.length === 0) return if (addresses.length === 0) return
dispatch(startListeningForBalance({ addresses })) dispatch(startListeningForBalance({ addresses }))
if (addresses.length > 0) {
return () => dispatch(stopListeningForBalance({ addresses })) return () => dispatch(stopListeningForBalance({ addresses }))
}, [addresses, dispatch]) }
}, [addresses, unchanged, dispatch])
const rawBalanceMap = useSelector<AppState>(({ wallet: { balances } }) => balances) const rawBalanceMap = useSelector<AppState>(({ wallet: { balances } }) => balances)
...@@ -57,15 +71,22 @@ export function useTokenBalances( ...@@ -57,15 +71,22 @@ export function useTokenBalances(
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const validTokens: Token[] = useMemo(() => tokens?.filter(t => isAddress(t?.address)) ?? [], [tokens]) const validTokens: Token[] = useMemo(() => tokens?.filter(t => isAddress(t?.address)) ?? [], [tokens])
const tokenAddresses: string[] = useMemo(() => validTokens.map(t => t.address).sort(), [validTokens])
const previousTokenAddresses = usePrevious(tokenAddresses)
const unchanged = JSON.stringify(tokenAddresses) === JSON.stringify(previousTokenAddresses)
// keep the listeners up to date // keep the listeners up to date
useEffect(() => { useEffect(() => {
if (address && validTokens.length > 0) { if (unchanged) return
const combos: TokenBalanceListenerKey[] = validTokens.map(token => ({ address, tokenAddress: token.address })) if (!address) return
if (tokenAddresses.length === 0) return
const combos: TokenBalanceListenerKey[] = tokenAddresses.map(tokenAddress => ({ address, tokenAddress }))
dispatch(startListeningForTokenBalances(combos)) dispatch(startListeningForTokenBalances(combos))
if (combos.length > 0) {
return () => dispatch(stopListeningForTokenBalances(combos)) return () => dispatch(stopListeningForTokenBalances(combos))
} }
}, [address, validTokens, dispatch]) }, [address, tokenAddresses, unchanged, dispatch])
const rawBalanceMap = useSelector<AppState>(({ wallet: { balances } }) => balances) const rawBalanceMap = useSelector<AppState>(({ wallet: { balances } }) => balances)
......
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