diff options
author | Alexander Tseung <alextsg@users.noreply.github.com> | 2018-12-10 04:48:06 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-12-10 04:48:06 +0800 |
commit | d8ab9cc002c10757b7382a174dafff7a0247e307 (patch) | |
tree | d0a46ac3ca2334ddec2ee240214d67a8122e81b7 /ui/app/selectors | |
parent | 575fb607c3b8deea831aa28293303991b3f6be29 (diff) | |
download | tangerine-wallet-browser-d8ab9cc002c10757b7382a174dafff7a0247e307.tar tangerine-wallet-browser-d8ab9cc002c10757b7382a174dafff7a0247e307.tar.gz tangerine-wallet-browser-d8ab9cc002c10757b7382a174dafff7a0247e307.tar.bz2 tangerine-wallet-browser-d8ab9cc002c10757b7382a174dafff7a0247e307.tar.lz tangerine-wallet-browser-d8ab9cc002c10757b7382a174dafff7a0247e307.tar.xz tangerine-wallet-browser-d8ab9cc002c10757b7382a174dafff7a0247e307.tar.zst tangerine-wallet-browser-d8ab9cc002c10757b7382a174dafff7a0247e307.zip |
Group transactions by nonce (#5886)
Diffstat (limited to 'ui/app/selectors')
-rw-r--r-- | ui/app/selectors/transactions.js | 232 |
1 files changed, 220 insertions, 12 deletions
diff --git a/ui/app/selectors/transactions.js b/ui/app/selectors/transactions.js index 479002794..301e8d11f 100644 --- a/ui/app/selectors/transactions.js +++ b/ui/app/selectors/transactions.js @@ -1,16 +1,44 @@ import { createSelector } from 'reselect' -import { valuesFor } from '../util' import { UNAPPROVED_STATUS, APPROVED_STATUS, SUBMITTED_STATUS, + CONFIRMED_STATUS, } from '../constants/transactions' +import { + TRANSACTION_TYPE_CANCEL, + TRANSACTION_TYPE_RETRY, +} from '../../../app/scripts/controllers/transactions/enums' +import { hexToDecimal } from '../helpers/conversions.util' import { selectedTokenAddressSelector } from './tokens' +import txHelper from '../../lib/tx-helper' export const shapeShiftTxListSelector = state => state.metamask.shapeShiftTxList export const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs export const selectedAddressTxListSelector = state => state.metamask.selectedAddressTxList +export const unapprovedPersonalMsgsSelector = state => state.metamask.unapprovedPersonalMsgs +export const unapprovedTypedMessagesSelector = state => state.metamask.unapprovedTypedMessages +export const networkSelector = state => state.metamask.network + +export const unapprovedMessagesSelector = createSelector( + unapprovedMsgsSelector, + unapprovedPersonalMsgsSelector, + unapprovedTypedMessagesSelector, + networkSelector, + ( + unapprovedMsgs = {}, + unapprovedPersonalMsgs = {}, + unapprovedTypedMessages = {}, + network + ) => txHelper( + {}, + unapprovedMsgs, + unapprovedPersonalMsgs, + unapprovedTypedMessages, + network + ) || [] +) const pendingStatusHash = { [UNAPPROVED_STATUS]: true, @@ -18,14 +46,18 @@ const pendingStatusHash = { [SUBMITTED_STATUS]: true, } +const priorityStatusHash = { + ...pendingStatusHash, + [CONFIRMED_STATUS]: true, +} + export const transactionsSelector = createSelector( selectedTokenAddressSelector, - unapprovedMsgsSelector, + unapprovedMessagesSelector, shapeShiftTxListSelector, selectedAddressTxListSelector, - (selectedTokenAddress, unapprovedMsgs = {}, shapeShiftTxList = [], transactions = []) => { - const unapprovedMsgsList = valuesFor(unapprovedMsgs) - const txsToRender = transactions.concat(unapprovedMsgsList, shapeShiftTxList) + (selectedTokenAddress, unapprovedMessages = [], shapeShiftTxList = [], transactions = []) => { + const txsToRender = transactions.concat(unapprovedMessages, shapeShiftTxList) return selectedTokenAddress ? txsToRender @@ -36,23 +68,199 @@ export const transactionsSelector = createSelector( } ) -export const pendingTransactionsSelector = createSelector( +/** + * @name insertOrderedNonce + * @private + * @description Inserts (mutates) a nonce into an array of ordered nonces, sorted in ascending + * order. + * @param {string[]} nonces - Array of nonce strings in hex + * @param {string} nonceToInsert - Nonce string in hex to be inserted into the array of nonces. + * @returns {string[]} + */ +const insertOrderedNonce = (nonces, nonceToInsert) => { + let insertIndex = nonces.length + + for (let i = 0; i < nonces.length; i++) { + const nonce = nonces[i] + + if (Number(hexToDecimal(nonce)) < Number(hexToDecimal(nonceToInsert))) { + insertIndex = i + break + } + } + + nonces.splice(insertIndex, 0, nonceToInsert) +} + +/** + * @name insertTransactionByTime + * @private + * @description Inserts (mutates) a transaction object into an array of ordered transactions, sorted + * in ascending order by time. + * @param {Object[]} transactions - Array of transaction objects. + * @param {Object} transaction - Transaction object to be inserted into the array of transactions. + * @returns {Object[]} + */ +const insertTransactionByTime = (transactions, transaction) => { + const { time } = transaction + + let insertIndex = transactions.length + + for (let i = 0; i < transactions.length; i++) { + const tx = transactions[i] + + if (tx.time > time) { + insertIndex = i + break + } + } + + transactions.splice(insertIndex, 0, transaction) +} + +/** + * Contains transactions and properties associated with those transactions of the same nonce. + * @typedef {Object} transactionGroup + * @property {string} nonce - The nonce that the transactions within this transactionGroup share. + * @property {Object[]} transactions - An array of transaction (txMeta) objects. + * @property {Object} initialTransaction - The transaction (txMeta) with the lowest "time". + * @property {Object} primaryTransaction - Either the latest transaction or the confirmed + * transaction. + * @property {boolean} hasRetried - True if a transaction in the group was a retry transaction. + * @property {boolean} hasCancelled - True if a transaction in the group was a cancel transaction. + */ + +/** + * @name insertTransactionGroupByTime + * @private + * @description Inserts (mutates) a transactionGroup object into an array of ordered + * transactionGroups, sorted in ascending order by nonce. + * @param {transactionGroup[]} transactionGroups - Array of transactionGroup objects. + * @param {transactionGroup} transactionGroup - transactionGroup object to be inserted into the + * array of transactionGroups. + * @returns {transactionGroup[]} + */ +const insertTransactionGroupByTime = (transactionGroups, transactionGroup) => { + const { primaryTransaction: { time } = {} } = transactionGroup + + let insertIndex = transactionGroups.length + + for (let i = 0; i < transactionGroups.length; i++) { + const txGroup = transactionGroups[i] + + if (txGroup.time > time) { + insertIndex = i + break + } + } + + transactionGroups.splice(insertIndex, 0, transactionGroup) +} + +/** + * @name nonceSortedTransactionsSelector + * @description Returns an array of transactionGroups sorted by nonce in ascending order. + * @returns {transactionGroup[]} + */ +export const nonceSortedTransactionsSelector = createSelector( transactionsSelector, + (transactions = []) => { + const unapprovedTransactionGroups = [] + const orderedNonces = [] + const nonceToTransactionsMap = {} + + transactions.forEach(transaction => { + const { txParams: { nonce } = {}, status, type, time: txTime } = transaction + + if (typeof nonce === 'undefined') { + const transactionGroup = { + transactions: [transaction], + initialTransaction: transaction, + primaryTransaction: transaction, + hasRetried: false, + hasCancelled: false, + } + + insertTransactionGroupByTime(unapprovedTransactionGroups, transactionGroup) + } else if (nonce in nonceToTransactionsMap) { + const nonceProps = nonceToTransactionsMap[nonce] + insertTransactionByTime(nonceProps.transactions, transaction) + + if (status in priorityStatusHash) { + const { primaryTransaction: { time: primaryTxTime = 0 } = {} } = nonceProps + + if (status === CONFIRMED_STATUS || txTime > primaryTxTime) { + nonceProps.primaryTransaction = transaction + } + } + + const { initialTransaction: { time: initialTxTime = 0 } = {} } = nonceProps + + // Used to display the transaction action, since we don't want to overwrite the action if + // it was replaced with a cancel attempt transaction. + if (txTime < initialTxTime) { + nonceProps.initialTransaction = transaction + } + + if (type === TRANSACTION_TYPE_RETRY) { + nonceProps.hasRetried = true + } + + if (type === TRANSACTION_TYPE_CANCEL) { + nonceProps.hasCancelled = true + } + } else { + nonceToTransactionsMap[nonce] = { + nonce, + transactions: [transaction], + initialTransaction: transaction, + primaryTransaction: transaction, + hasRetried: transaction.type === TRANSACTION_TYPE_RETRY, + hasCancelled: transaction.type === TRANSACTION_TYPE_CANCEL, + } + + insertOrderedNonce(orderedNonces, nonce) + } + }) + + const orderedTransactionGroups = orderedNonces.map(nonce => nonceToTransactionsMap[nonce]) + return unapprovedTransactionGroups.concat(orderedTransactionGroups) + } +) + +/** + * @name nonceSortedPendingTransactionsSelector + * @description Returns an array of transactionGroups where transactions are still pending sorted by + * nonce in descending order. + * @returns {transactionGroup[]} + */ +export const nonceSortedPendingTransactionsSelector = createSelector( + nonceSortedTransactionsSelector, (transactions = []) => ( - transactions.filter(transaction => transaction.status in pendingStatusHash).reverse() + transactions + .filter(({ primaryTransaction }) => primaryTransaction.status in pendingStatusHash) + .reverse() ) ) -export const submittedPendingTransactionsSelector = createSelector( - transactionsSelector, +/** + * @name nonceSortedCompletedTransactionsSelector + * @description Returns an array of transactionGroups where transactions are confirmed sorted by + * nonce in descending order. + * @returns {transactionGroup[]} + */ +export const nonceSortedCompletedTransactionsSelector = createSelector( + nonceSortedTransactionsSelector, (transactions = []) => ( - transactions.filter(transaction => transaction.status === SUBMITTED_STATUS) + transactions.filter(({ primaryTransaction }) => { + return !(primaryTransaction.status in pendingStatusHash) + }) ) ) -export const completedTransactionsSelector = createSelector( +export const submittedPendingTransactionsSelector = createSelector( transactionsSelector, (transactions = []) => ( - transactions.filter(transaction => !(transaction.status in pendingStatusHash)) + transactions.filter(transaction => transaction.status === SUBMITTED_STATUS) ) ) |