Improve the transaction render style (#767)

This commit is contained in:
Moody Salem 2020-05-14 10:32:48 -04:00 committed by GitHub
parent 4ba7dd9535
commit 5405648a54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 140 additions and 125 deletions

@ -1,61 +1,36 @@
import React from 'react'
import styled, { keyframes } from 'styled-components'
import styled from 'styled-components'
import { Check, Triangle } from 'react-feather'
import { useWeb3React } from '../../hooks'
import { getEtherscanLink } from '../../utils'
import { Link, Spinner } from '../../theme'
import Copy from './Copy'
import Circle from '../../assets/images/circle.svg'
import { transparentize } from 'polished'
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`
${({ theme }) => theme.flexRowNoWrap}
justify-content: space-between;
width: 100%;
margin-top: 0.75rem;
a {
min-width: 0;
word-break: break-word;
}
`
const TransactionStatusText = styled.span`
margin-left: 0.5rem;
word-break: keep-all;
const TransactionStatusText = styled.div`
margin-right: 0.5rem;
`
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
const TransactionState = styled.div<{ pending: boolean; success?: boolean }>`
const TransactionState = styled(Link)<{ pending: boolean; success?: boolean }>`
display: flex;
background-color: ${({ pending, success, theme }) =>
pending
? transparentize(0.95, theme.primary1)
: success
? transparentize(0.95, theme.green1)
: transparentize(0.95, theme.red1)};
border-radius: 1.5rem;
justify-content: space-between;
text-decoration: none !important;
border-radius: 0.5rem;
padding: 0.5rem 0.75rem;
font-weight: 500;
font-size: 0.75rem;
border: 1px solid;
color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)};
border-color: ${({ pending, success, theme }) =>
pending
? transparentize(0.75, theme.primary1)
@ -63,10 +38,6 @@ const TransactionState = styled.div<{ pending: boolean; success?: boolean }>`
? transparentize(0.75, theme.green1)
: transparentize(0.75, theme.red1)};
#pending {
animation: 2s ${rotate} linear infinite;
}
:hover {
border-color: ${({ pending, success, theme }) =>
pending
@ -76,11 +47,6 @@ const TransactionState = styled.div<{ pending: boolean; success?: boolean }>`
: 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 }) {
const { chainId } = useWeb3React()
@ -93,19 +59,11 @@ export default function Transaction({ hash }: { hash: string }) {
(allTransactions[hash].receipt.status === 1 || typeof allTransactions[hash].receipt.status === 'undefined')
return (
<TransactionWrapper key={hash}>
<TransactionStatusWrapper>
<Link href={getEtherscanLink(chainId, hash, 'transaction')}>{summary ? summary : hash} </Link>
<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" />}
<TransactionStatusText>{pending ? 'Pending' : success ? 'Success' : 'Failed'}</TransactionStatusText>
</TransactionState>
</Link>
</ButtonWrapper>
<TransactionWrapper>
<TransactionState href={getEtherscanLink(chainId, hash, 'transaction')} pending={pending} success={success}>
<TransactionStatusText>{summary ? summary : hash}</TransactionStatusText>
{pending ? <Spinner src={Circle} /> : success ? <Check size="16" /> : <Triangle size="16" />}
</TransactionState>
</TransactionWrapper>
)
}

@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React, { useCallback, useState } from 'react'
import { AlertCircle, CheckCircle } from 'react-feather'
@ -43,10 +43,11 @@ export default function TxnPopup({
const [isRunning, setIsRunning] = useState(true)
const removePopup = useRemovePopup()
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
useInterval(
() => {
count > 150 && removePopup(popKey)
setCount(count + 1)
count > 150 ? removeThisPopup() : setCount(count + 1)
},
isRunning ? delay : null
)

@ -95,7 +95,8 @@ const Web3StatusConnected = styled(Web3StatusGeneric)<{ pending?: boolean }>`
border: 1px solid ${({ pending, theme }) => (pending ? theme.primary1 : theme.bg3)};
color: ${({ pending, theme }) => (pending ? theme.white : theme.text1)};
font-weight: 500;
:hover {
:hover,
:focus {
background-color: ${({ pending, theme }) => (pending ? darken(0.05, theme.primary1) : lighten(0.05, theme.bg2))};
:focus {

@ -14,6 +14,7 @@ export interface SerializableTransactionReceipt {
export const addTransaction = createAction<{
chainId: number
hash: string
from: string
approvalOfToken?: string
summary?: string
}>('addTransaction')
@ -23,3 +24,7 @@ export const finalizeTransaction = createAction<{
hash: string
receipt: SerializableTransactionReceipt
}>('finalizeTransaction')
export const updateTransactionCount = createAction<{ address: string; transactionCount: number; chainId: number }>(
'updateTransactionCount'
)

@ -12,7 +12,7 @@ export function useTransactionAdder(): (
response: TransactionResponse,
customData?: { summary?: string; approvalOfToken?: string }
) => void {
const { chainId } = useWeb3React()
const { chainId, account } = useWeb3React()
const dispatch = useDispatch<AppDispatch>()
return useCallback(
@ -24,9 +24,9 @@ export function useTransactionAdder(): (
if (!hash) {
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]
)
}

@ -1,5 +1,12 @@
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()
@ -11,6 +18,11 @@ export interface TransactionDetails {
receipt?: SerializableTransactionReceipt
addedTime: 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 {
@ -23,19 +35,19 @@ const initialState: TransactionState = {}
export default createReducer(initialState, builder =>
builder
.addCase(addTransaction, (state, { payload: { chainId, hash, approvalOfToken, summary } }) => {
.addCase(addTransaction, (state, { payload: { chainId, from, hash, approvalOfToken, summary } }) => {
if (state[chainId]?.[hash]) {
throw Error('Attempted to add existing transaction.')
}
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 } }) => {
if (!state[chainId]?.[hash]) {
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 } }) => {
if (!state[chainId]?.[hash]) {
@ -43,6 +55,17 @@ export default createReducer(initialState, builder =>
}
state[chainId] = state[chainId] ?? {}
state[chainId][hash].receipt = receipt
state[chainId][hash].unknownStatus = false
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'
import { useWeb3React } from '../../hooks'
import { useAddPopup, useBlockNumber } from '../application/hooks'
import { AppDispatch, AppState } from '../index'
import { checkTransaction, finalizeTransaction } from './actions'
import { checkTransaction, finalizeTransaction, updateTransactionCount } from './actions'
import useSWR from 'swr'
export default function Updater() {
const { chainId, library } = useWeb3React()
const { chainId, account, library } = useWeb3React()
const globalBlockNumber = useBlockNumber()
const lastBlockNumber = useBlockNumber()
const dispatch = useDispatch<AppDispatch>()
const transactions = useSelector<AppState>(state => state.transactions)
@ -18,64 +19,69 @@ export default function Updater() {
// show popup on confirm
const addPopup = useAddPopup()
const { data: transactionCount } = useSWR<number | null>(['accountNonce', account, lastBlockNumber], () => {
if (!account) return null
return library.getTransactionCount(account, 'latest')
})
useEffect(() => {
if ((chainId || chainId === 0) && library) {
let stale = false
if (transactionCount === null) return
dispatch(updateTransactionCount({ address: account, transactionCount, chainId }))
}, [transactionCount, account, chainId, dispatch])
useEffect(() => {
if (typeof chainId === 'number' && library) {
Object.keys(allTransactions)
.filter(hash => !allTransactions[hash].receipt)
.filter(
hash => !allTransactions[hash].receipt && allTransactions[hash].blockNumberChecked !== globalBlockNumber
hash =>
!allTransactions[hash].blockNumberChecked || allTransactions[hash].blockNumberChecked < lastBlockNumber
)
.forEach(hash => {
library
.getTransactionReceipt(hash)
.then(receipt => {
if (!stale) {
if (!receipt) {
dispatch(checkTransaction({ chainId, hash, blockNumber: globalBlockNumber }))
} else {
dispatch(
finalizeTransaction({
chainId,
if (!receipt) {
dispatch(checkTransaction({ chainId, hash, blockNumber: lastBlockNumber }))
} else {
dispatch(
finalizeTransaction({
chainId,
hash,
receipt: {
blockHash: receipt.blockHash,
blockNumber: receipt.blockNumber,
contractAddress: receipt.contractAddress,
from: receipt.from,
status: receipt.status,
to: receipt.to,
transactionHash: receipt.transactionHash,
transactionIndex: receipt.transactionIndex
}
})
)
// add success or failure popup
if (receipt.status === 1) {
addPopup({
txn: {
hash,
receipt: {
blockHash: receipt.blockHash,
blockNumber: receipt.blockNumber,
contractAddress: receipt.contractAddress,
from: receipt.from,
status: receipt.status,
to: receipt.to,
transactionHash: receipt.transactionHash,
transactionIndex: receipt.transactionIndex
}
})
)
// add success or failure popup
if (receipt.status === 1) {
addPopup({
txn: {
hash,
success: true,
summary: allTransactions[hash]?.summary
}
})
} else {
addPopup({
txn: { hash, success: false, summary: allTransactions[hash]?.summary }
})
}
success: true,
summary: allTransactions[hash]?.summary
}
})
} else {
addPopup({
txn: { hash, success: false, summary: allTransactions[hash]?.summary }
})
}
}
})
.catch(() => {
dispatch(checkTransaction({ chainId, hash, blockNumber: globalBlockNumber }))
.catch(error => {
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
}

@ -1,8 +1,9 @@
import { getAddress } from '@ethersproject/address'
import { JSBI, Token, TokenAmount, WETH } from '@uniswap/sdk'
import { useEffect, useMemo } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useAllTokens } from '../../contexts/Tokens'
import { useWeb3React } from '../../hooks'
import { usePrevious, useWeb3React } from '../../hooks'
import { isAddress } from '../../utils'
import { AppDispatch, AppState } from '../index'
import {
@ -21,16 +22,29 @@ export function useETHBalances(uncheckedAddresses?: (string | undefined)[]): { [
const dispatch = useDispatch<AppDispatch>()
const { chainId } = useWeb3React()
const addresses: string[] = useMemo(() => (uncheckedAddresses ? uncheckedAddresses.filter(isAddress) : []), [
uncheckedAddresses
])
const addresses: string[] = useMemo(
() =>
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
useEffect(() => {
if (unchanged) return
if (addresses.length === 0) return
dispatch(startListeningForBalance({ addresses }))
return () => dispatch(stopListeningForBalance({ addresses }))
}, [addresses, dispatch])
if (addresses.length > 0) {
return () => dispatch(stopListeningForBalance({ addresses }))
}
}, [addresses, unchanged, dispatch])
const rawBalanceMap = useSelector<AppState>(({ wallet: { balances } }) => balances)
@ -57,15 +71,22 @@ export function useTokenBalances(
const { chainId } = useWeb3React()
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
useEffect(() => {
if (address && validTokens.length > 0) {
const combos: TokenBalanceListenerKey[] = validTokens.map(token => ({ address, tokenAddress: token.address }))
dispatch(startListeningForTokenBalances(combos))
if (unchanged) return
if (!address) return
if (tokenAddresses.length === 0) return
const combos: TokenBalanceListenerKey[] = tokenAddresses.map(tokenAddress => ({ address, tokenAddress }))
dispatch(startListeningForTokenBalances(combos))
if (combos.length > 0) {
return () => dispatch(stopListeningForTokenBalances(combos))
}
}, [address, validTokens, dispatch])
}, [address, tokenAddresses, unchanged, dispatch])
const rawBalanceMap = useSelector<AppState>(({ wallet: { balances } }) => balances)