Improve the transaction render style (#767)
This commit is contained in:
parent
4ba7dd9535
commit
5405648a54
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user