Skip to content

Commit

Permalink
adds pending InvocationTransaction UX and logic flow and separates br…
Browse files Browse the repository at this point in the history
…eaks down abstract transaction components
  • Loading branch information
comountainclimber committed Dec 30, 2018
1 parent 75550f4 commit ad7d167
Show file tree
Hide file tree
Showing 10 changed files with 409 additions and 239 deletions.
126 changes: 94 additions & 32 deletions app/actions/pendingTransactionActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,30 @@ import { createActions } from 'spunky'
import Neon from '@cityofzion/neon-js'
import { isEmpty } from 'lodash-es'

import { toBigNumber } from '../core/math'
import { getStorage, setStorage } from '../core/storage'
import { getNode, getRPCEndpoint } from './nodeStorageActions'
import { findAndReturnTokenInfo } from '../util/findAndReturnTokenInfo'
import {
findAndReturnTokenInfo,
getImageBySymbol,
} from '../util/findAndReturnTokenInfo'

export const ID = 'pendingTransactions'
const STORAGE_KEY = 'pendingTransactions'
const MINIMUM_CONFIRMATIONS = 40
const MINIMUM_CONFIRMATIONS = 10

type PendingTransactions = {
[address: string]: Array<string>,
[address: string]: Array<any>,
}

type PendingTransaction = {
vout: Array<{ asset: string, address: string, value: string }>,
sendEntries: Array<SendEntryType>,
confirmations: number,
txid: number,
net_fee: string,
blocktime: number,
type: string,
}

type ParsedPendingTransaction = {
Expand All @@ -30,32 +36,78 @@ type ParsedPendingTransaction = {
blocktime: number,
to: string,
amount: string,
asset: string,
asset: {
symbol: string,
image: string,
},
}

export const parseContractTransaction = async (
transaction: PendingTransaction,
net: string,
): Promise<Array<ParsedPendingTransaction>> => {
const parsedData = []
// eslint-disable-next-line camelcase
const { confirmations, txid, net_fee, blocktime = 0 } = transaction
transaction.vout.pop()
// eslint-disable-next-line
for (const send of transaction.vout) {
parsedData.push({
confirmations,
txid,
net_fee,
blocktime,
amount: toBigNumber(send.value).toString(),
to: send.address,
// eslint-disable-next-line no-await-in-loop
asset: await findAndReturnTokenInfo(send.asset, net),
})
}
return parsedData
}

export const parseInvocationTransaction = (
transaction: PendingTransaction,
): Array<ParsedPendingTransaction> => {
const {
confirmations,
txid,
// eslint-disable-next-line camelcase
net_fee,
blocktime = 0,
sendEntries,
} = transaction

// things get tricky during invocation transactions as there is no vout array
// and it is not straight forward parsing the produced script. Instead we
// use the original send entries array.
return sendEntries.map(send => ({
confirmations,
txid,
net_fee,
blocktime,
amount: toBigNumber(send.amount).toString(),
to: send.address,
asset: {
symbol: send.symbol,
image: getImageBySymbol(send.symbol),
},
}))
}

export const parsePendingTxInfo = async (
export const parsePendingContractTxInfo = async (
pendingTransactionsInfo: Array<PendingTransaction>,
net: string,
) => {
const parsedData: Array<ParsedPendingTransaction> = []
// eslint-disable-next-line
for (const transaction of pendingTransactionsInfo) {
if (transaction) {
// eslint-disable-next-line
const { confirmations, txid, net_fee, blocktime = 0 } = transaction
transaction.vout.pop()
// eslint-disable-next-line
for (const send of transaction.vout) {
parsedData.push({
confirmations,
txid,
net_fee,
blocktime,
amount: send.value,
to: send.address,
// eslint-disable-next-line no-await-in-loop
asset: await findAndReturnTokenInfo(send.asset, net),
})
if (transaction.type === 'InvocationTransaction') {
parsedData.push(...parseInvocationTransaction(transaction))
} else {
// eslint-disable-next-line no-await-in-loop
parsedData.push(...(await parseContractTransaction(transaction, net)))
}
}
}
Expand Down Expand Up @@ -83,21 +135,21 @@ export const pruneConfirmedOrStaleTransaction = async (
const storage = await getPendingTransactions()
if (Array.isArray(storage[address])) {
storage[address] = storage[address].filter(
transaction => transaction !== txId,
transaction => transaction.hash !== txId,
)
}
await setPendingTransactions(storage)
}

export const addPendingTransaction = createActions(
ID,
({ address, txId }) => async (): Promise<void> => {
({ address, tx }) => async (): Promise<void> => {
const transactions = await getPendingTransactions()

if (Array.isArray(transactions[address])) {
transactions[address].push(txId)
transactions[address].push(tx)
} else {
transactions[address] = []
transactions[address] = [tx]
}
await setPendingTransactions(transactions)
},
Expand All @@ -121,24 +173,34 @@ export const getPendingTransactionInfo = createActions(
if (transaction) {
// eslint-disable-next-line
const result = await client
.getRawTransaction(transaction, 1)
.getRawTransaction(transaction.hash, 1)
.catch(async e => {
console.error(
e,
'An transaction was added to storage that the blockchain does not recognize - purging from storage',
`Error performing getRawTransaction for txid: ${
transaction.hash
}`,
)
await pruneConfirmedOrStaleTransaction(address, transaction)
if (e.message === 'Unknown transaction') {
await pruneConfirmedOrStaleTransaction(
address,
transaction.hash,
)
}
})

if (result.confirmations > MINIMUM_CONFIRMATIONS) {
// eslint-disable-next-line
await pruneConfirmedOrStaleTransaction(address, transaction)
if (result) {
if (result.confirmations > MINIMUM_CONFIRMATIONS) {
// eslint-disable-next-line
await pruneConfirmedOrStaleTransaction(address, transaction.hash)
} else {
pendingTransactionInfo.push({ ...result, ...transaction })
}
}
pendingTransactionInfo.push(result)
}
}

return parsePendingTxInfo(pendingTransactionInfo, net)
return parsePendingContractTxInfo(pendingTransactionInfo, net)
}
return []
},
Expand Down
61 changes: 61 additions & 0 deletions app/components/Blockchain/Transaction/ClaimAbstract.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// @flow
import React, { Fragment } from 'react'

import classNames from 'classnames'
import styles from './Transaction.scss'
import ClaimIcon from '../../../assets/icons/claim.svg'
import CopyToClipboard from '../../CopyToClipboard'

type Props = {
txDate: React$Node,
logo: React$Node,
label: string,
amount: string | number,
contactTo: React$Node | string,
to: string,
contactToExists: boolean,
}

export default class ClaimAbstract extends React.Component<Props> {
render = () => {
const {
txDate,
logo,
label,
amount,
contactTo,
to,
contactToExists,
} = this.props
return (
<div className={classNames(styles.transactionContainer)}>
<div className={styles.abstractContainer}>
<div className={styles.txTypeIconContainer}>
<div className={styles.claimIconContainer}>
<ClaimIcon />
</div>
</div>
{txDate}
<div className={styles.txLabelContainer}>
{logo}
{label}
</div>
<div className={styles.txAmountContainer}>{amount}</div>
<div className={styles.txToContainer}>
<Fragment>
<span>{contactTo}</span>
{!contactToExists && (
<CopyToClipboard
className={styles.copy}
text={to}
tooltip="Copy Public Address"
/>
)}
</Fragment>
</div>
<div className={styles.historyButtonPlaceholder} />
</div>
</div>
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@ import CopyToClipboard from '../../CopyToClipboard'
import { pluralize } from '../../../util/pluralize'

type Props = {
renderTxDate: (time: ?number) => React$Node | null,
txDate: React$Node | null,
findContact: (address: string) => React$Node | null,
asset: {
symbol: string,
image?: string,
},
blocktime: number,
amount: string,
amount: string | number,
to: string,
confirmations: number,
showAddContactModal: (address: string) => void,
}

export default class Transaction extends React.Component<Props> {
export default class PendingAbstract extends React.Component<Props> {
render = () => {
const {
asset,
Expand All @@ -32,6 +32,7 @@ export default class Transaction extends React.Component<Props> {
findContact,
showAddContactModal,
confirmations,
txDate,
} = this.props
const contactTo = findContact(to)
const contactToExists = contactTo !== to
Expand All @@ -47,7 +48,14 @@ export default class Transaction extends React.Component<Props> {
<SendIcon />
</div>
</div>
{!!blocktime && this.props.renderTxDate(blocktime)}
{!blocktime ? (
<div className={styles.pendingTxDate}>
awaiting confirmations...
</div>
) : (
txDate
)}

<div className={styles.txLabelContainer}>
{logo}
{asset.symbol}
Expand Down
80 changes: 80 additions & 0 deletions app/components/Blockchain/Transaction/ReceiveAbstract.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// @flow
import React from 'react'
import classNames from 'classnames'

import Button from '../../Button'
import styles from './Transaction.scss'
import ReceiveIcon from '../../../assets/icons/receive-tx.svg'
import ContactsAdd from '../../../assets/icons/contacts-add.svg'
import CopyToClipboard from '../../CopyToClipboard'

type Props = {
txDate: React$Node,
logo: React$Node,
label: string,
amount: string | number,
showAddContactModal: (from: string) => void,
contactFromExists: boolean,
from: string,
address: string,
contactFrom: React$Node | string,
contactFromExists: boolean,
}

export default class ReceiveAbstract extends React.Component<Props> {
render = () => {
const {
txDate,
logo,
label,
amount,
contactFrom,
showAddContactModal,
contactFromExists,
from,
address,
} = this.props
const isMintTokens = from === 'MINT TOKENS'
const isGasClaim = address === from && !Number(amount)
return (
<div className={classNames(styles.transactionContainer)}>
<div className={styles.abstractContainer}>
<div className={styles.txTypeIconContainer}>
<div className={styles.receiveIconContainer}>
<ReceiveIcon />
</div>
</div>
{txDate}
<div className={styles.txLabelContainer}>
{logo}
{label}
</div>
<div className={styles.txAmountContainer}>{amount}</div>
<div className={styles.txToContainer}>
<span>{contactFrom}</span>
{!contactFromExists &&
!isMintTokens && (
<CopyToClipboard
className={styles.copy}
text={from}
tooltip="Copy Public Address"
/>
)}
</div>
{isMintTokens || isGasClaim ? (
<div className={styles.transactionHistoryButton} />
) : (
<Button
className={styles.transactionHistoryButton}
renderIcon={ContactsAdd}
onClick={() => showAddContactModal(from)}
disabled={contactFromExists}
>
Add
</Button>
)}
</div>
</div>
)
}
}

0 comments on commit ad7d167

Please sign in to comment.