Skip to content

Commit

Permalink
Pending transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
lubej committed May 27, 2024
1 parent a70bdbf commit 1b5b7d4
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 21 deletions.
49 changes: 36 additions & 13 deletions src/app/components/Transaction/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const StyledCardBody = styled(CardBody)`

interface TransactionProps {
referenceAddress: string
transaction: transactionTypes.Transaction
transaction: Partial<transactionTypes.Transaction>
network: NetworkType
}

Expand Down Expand Up @@ -396,9 +396,9 @@ export function Transaction(props: TransactionProps) {
const backendLinks = config[props.network][backend()]
const externalExplorerLink = transaction.runtimeId
? backendLinks.blockExplorerParatimes
?.replace('{{txHash}}', encodeURIComponent(transaction.hash))
?.replace('{{txHash}}', encodeURIComponent(transaction.hash!))
.replace('{{runtimeId}}', encodeURIComponent(transaction.runtimeId))
: backendLinks.blockExplorer.replace('{{txHash}}', encodeURIComponent(transaction.hash))
: backendLinks.blockExplorer.replace('{{txHash}}', encodeURIComponent(transaction.hash!))

return (
<Card
Expand Down Expand Up @@ -451,14 +451,16 @@ export function Transaction(props: TransactionProps) {
icon={Package}
label={t('common.hash', 'Tx Hash')}
>
<Text>{trimLongString(transaction.hash)}</Text>
<Text>{trimLongString(transaction.hash!)}</Text>
</InfoBox>
</Box>

<Box pad="none">
<InfoBox icon={Clock} label={t('common.time', 'Time')}>
{intlDateTimeFormat(transaction.timestamp!)}
</InfoBox>
{transaction.timestamp && (
<InfoBox icon={Clock} label={t('common.time', 'Time')}>
{intlDateTimeFormat(transaction.timestamp)}
</InfoBox>
)}

{!transaction.runtimeId && transaction.level && (
<InfoBox icon={Cube} label={t('common.block', 'Block')}>
Expand All @@ -479,12 +481,33 @@ export function Transaction(props: TransactionProps) {
<Text weight="bold" size={isMobile ? 'medium' : 'xlarge'}>
<AmountFormatter amount={transaction.amount!} smallTicker />
</Text>
<Text color={transaction.status ? 'successful-label' : 'status-error'} size="small" weight="bold">
{transaction.status ? (
<span>{t('account.transaction.successful', 'Successful')}</span>
) : (
<span>{t('account.transaction.failed', 'Failed')}</span>
)}
<Text
color={(() => {
switch (transaction.status) {
case true:
return 'successful-label'
case false:
return 'status-error'
case undefined:
default:
return 'status-warning'
}
})()}
size="small"
weight="bold"
>
{(() => {
switch (transaction.status) {
case true:
return <span>{t('account.transaction.successful', 'Successful')}</span>
case false:
return <span>{t('account.transaction.failed', 'Failed')}</span>
case undefined:
default:
/* TODO: Add translation */
return <span>Pending</span>
}
})()}
</Text>
</Box>
</StyledCardBody>
Expand Down
28 changes: 21 additions & 7 deletions src/app/pages/AccountPage/Features/TransactionHistory/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable no-restricted-syntax */

/**
*
* TransactionHistory
Expand All @@ -17,32 +19,44 @@ import {
} from 'app/state/account/selectors'
import { selectSelectedNetwork } from 'app/state/network/selectors'
import { ErrorFormatter } from 'app/components/ErrorFormatter'

interface Props {}
import { selectPendingTransactionForAccount } from '../../../../state/transaction/selectors'

/**
* Displays the past transactions from the state for a given account
*/
export function TransactionHistory(props: Props) {
export function TransactionHistory() {
const { t } = useTranslation()
const allTransactions = useSelector(selectTransactions)
const transactionsError = useSelector(selectTransactionsError)
const address = useSelector(selectAccountAddress)
// TODO: Remove pending transaction once it is available in allTransactions in transactionsLoaded
const pendingTransactions = useSelector(state => selectPendingTransactionForAccount(state, address))
const network = useSelector(selectSelectedNetwork)
const transactionComponents = allTransactions.map((t, i) => (
<Transaction key={i} transaction={t} referenceAddress={address} network={network} />
const transactionComponents = allTransactions.map(t => (
<Transaction key={t.hash} transaction={t} referenceAddress={address} network={network} />
))
const pendingTransactionComponents = pendingTransactions
.filter(({ hash: pendingTxHash }) => !allTransactions.some(({ hash }) => hash === pendingTxHash))
.map(t => <Transaction key={t.hash} transaction={t} referenceAddress={address} network={network} />)

return (
<Box gap="medium" margin="none">
{transactionsError && (
<p>
{t('account.transaction.loadingError', "Couldn't load transactions.")}{' '}
{t('account.transaction.loadingError', `Couldn't load transactions.`)}{' '}
<ErrorFormatter code={transactionsError.code} message={transactionsError.message} />
</p>
)}
{!!pendingTransactionComponents.length && (
<>
{/*TODO: Translation*/}
<Heading level="3">Pending transactions</Heading>
{pendingTransactionComponents}
</>
)}
{/*TODO: Translation*/}
<Heading level="3">Activity</Heading>
{allTransactions.length ? (
// eslint-disable-next-line no-restricted-syntax -- transactionComponents is not a plain text node
transactionComponents
) : (
<Box
Expand Down
26 changes: 26 additions & 0 deletions src/app/state/transaction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import {
TransactionStep,
TransactionPayload,
ReclaimEscrowPayload,
PendingTransactionPayload,
} from './types'

export const initialState: TransactionState = {
success: false,
active: false,
pendingTransactions: {},
}

export const transactionSlice = createSlice({
Expand Down Expand Up @@ -60,6 +62,30 @@ export const transactionSlice = createSlice({
state.error = action.payload
state.active = false
},
addPendingTransaction(state, action: PayloadAction<PendingTransactionPayload>) {
// TODO: Add NetworkType differentiation
const {
payload: { from, transaction },
} = action

if (!state.pendingTransactions[from]) {
state.pendingTransactions[from] = [transaction]
} else {
state.pendingTransactions[from] = [...state.pendingTransactions[from], transaction]
}
},
removePendingTransaction(state, action: PayloadAction<PendingTransactionPayload>) {
// TODO: Add NetworkType differentiation
const {
payload: { from, transaction },
} = action

if (state.pendingTransactions[from]?.length) {
state.pendingTransactions[from] = state.pendingTransactions[from].filter(
({ hash }) => hash !== transaction.hash,
)
}
},
},
})

Expand Down
36 changes: 35 additions & 1 deletion src/app/state/transaction/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { selectAccountAddress, selectAccountAllowances } from '../account/select
import { selectChainContext } from '../network/selectors'
import { selectActiveWallet } from '../wallet/selectors'
import { Wallet, WalletType } from '../wallet/types'
import { TransactionPayload, TransactionStep } from './types'
import { Transaction, TransactionPayload, TransactionStep, TransactionType } from './types'
import { ParaTimeTransaction, Runtime, TransactionTypes } from '../paratimes/types'

export function* transactionSaga() {
Expand Down Expand Up @@ -201,6 +201,40 @@ export function* doTransaction(action: PayloadAction<TransactionPayload>) {

// Notify that the transaction was a success
yield* put(transactionActions.transactionSent(action.payload))

const hash = yield* call([tw, tw.hash])

const transaction: Transaction = {
status: undefined,
fee: undefined,
level: undefined,
to: undefined,
round: undefined,
runtimeId: undefined,
runtimeName: undefined,
timestamp: undefined,
hash,
type: tw.transaction.method as TransactionType,
from: activeWallet.address,
amount: action.payload.amount,
...(action.payload.type === 'transfer'
? {
to: action.payload.to,
}
: {}),
...(action.payload.type === 'addEscrow'
? {
to: action.payload.validator,
}
: {}),
...(action.payload.type === 'reclaimEscrow'
? {
to: action.payload.validator,
}
: {}),
}

yield* put(transactionActions.addPendingTransaction({ transaction, from: activeWallet.address }))
} catch (e: any) {
let payload: ErrorPayload
if (e instanceof WalletError) {
Expand Down
4 changes: 4 additions & 0 deletions src/app/state/transaction/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ export const selectTransactionPreview = createSelector(
[selectTransaction],
transaction => transaction.preview,
)
export const selectPendingTransactionForAccount = createSelector(
[selectSlice, (_, address) => address],
(state, address) => state.pendingTransactions[address] ?? [],
)
14 changes: 14 additions & 0 deletions src/app/state/transaction/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ export interface TransactionState {
success: boolean
error?: ErrorPayload
preview?: TransactionPreview
// TODO: Should pendingTransactions be persisted?
/**
* Keeps track of pending transactions.
*
* @typedef {Object} pendingTransactions
* @property {Object<string, string[]>} [pendingTransactions] - A dictionary where the keys are addresses
* and the value is array of pending transactions.
*/
pendingTransactions: Record<string, Transaction[]>
}

export type NewTransactionType = 'transfer' | 'addEscrow' | 'reclaimEscrow'
Expand Down Expand Up @@ -110,3 +119,8 @@ export interface ReclaimEscrowPayload {
/* Displayed token equivalent */
amount: StringifiedBigInt
}

export interface PendingTransactionPayload {
from: string
transaction: Transaction
}

0 comments on commit 1b5b7d4

Please sign in to comment.