mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 09:57:02 +01:00
Group transactions by nonce (#5886)
This commit is contained in:
parent
575fb607c3
commit
d8ab9cc002
@ -17,6 +17,9 @@
|
||||
"confirmClear": {
|
||||
"message": "Are you sure you want to clear approved websites?"
|
||||
},
|
||||
"contractInteraction": {
|
||||
"message": "Contract Interaction"
|
||||
},
|
||||
"clearApprovalDataSuccess": {
|
||||
"message": "Approved website data cleared successfully."
|
||||
},
|
||||
@ -185,6 +188,9 @@
|
||||
"cancellationGasFee": {
|
||||
"message": "Cancellation Gas Fee"
|
||||
},
|
||||
"cancelled": {
|
||||
"message": "Cancelled"
|
||||
},
|
||||
"cancelN": {
|
||||
"message": "Cancel all $1 transactions"
|
||||
},
|
||||
@ -1177,6 +1183,12 @@
|
||||
"speedUpSubtitle": {
|
||||
"message": "Increase your gas price to attempt to overwrite and speed up your transaction"
|
||||
},
|
||||
"speedUpCancellation": {
|
||||
"message": "Speed up this cancellation"
|
||||
},
|
||||
"speedUpTransaction": {
|
||||
"message": "Speed up this transaction"
|
||||
},
|
||||
"status": {
|
||||
"message": "Status"
|
||||
},
|
||||
@ -1263,29 +1275,38 @@
|
||||
"message": "transaction"
|
||||
},
|
||||
"transactionConfirmed": {
|
||||
"message": "Transaction confirmed on $2."
|
||||
"message": "Transaction confirmed at $2."
|
||||
},
|
||||
"transactionCreated": {
|
||||
"message": "Transaction created with a value of $1 on $2."
|
||||
"message": "Transaction created with a value of $1 at $2."
|
||||
},
|
||||
"transactionWithNonce": {
|
||||
"message": "Transaction $1"
|
||||
},
|
||||
"transactionDropped": {
|
||||
"message": "Transaction dropped on $2."
|
||||
"message": "Transaction dropped at $2."
|
||||
},
|
||||
"transactionSubmitted": {
|
||||
"message": "Transaction submitted on $2."
|
||||
"message": "Transaction submitted with gas fee of $1 at $2."
|
||||
},
|
||||
"transactionResubmitted": {
|
||||
"message": "Transaction resubmitted with gas fee increased to $1 at $2"
|
||||
},
|
||||
"transactionUpdated": {
|
||||
"message": "Transaction updated on $2."
|
||||
"message": "Transaction updated at $2."
|
||||
},
|
||||
"transactionUpdatedGas": {
|
||||
"message": "Transaction updated with a gas price of $1 on $2."
|
||||
"message": "Transaction updated with a gas fee of $1 at $2."
|
||||
},
|
||||
"transactionErrored": {
|
||||
"message": "Transaction encountered an error."
|
||||
},
|
||||
"transactionCancelAttempted": {
|
||||
"message": "Transaction cancel attempted with gas fee of $1 at $2"
|
||||
},
|
||||
"transactionCancelSuccess": {
|
||||
"message": "Transaction successfully cancelled at $2"
|
||||
},
|
||||
"transactions": {
|
||||
"message": "transactions"
|
||||
},
|
||||
@ -1350,9 +1371,6 @@
|
||||
"unknown": {
|
||||
"message": "Unknown"
|
||||
},
|
||||
"unknownFunction": {
|
||||
"message": "Unknown Function"
|
||||
},
|
||||
"unknownNetwork": {
|
||||
"message": "Unknown Private Network"
|
||||
},
|
||||
|
3
app/images/icons/cancelled.svg
Executable file
3
app/images/icons/cancelled.svg
Executable file
@ -0,0 +1,3 @@
|
||||
<svg width="7" height="8" viewBox="0 0 7 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.97959 1.19291C1.66717 0.880488 1.16063 0.880488 0.848215 1.19291C0.535796 1.50533 0.535796 2.01186 0.848215 2.32428L2.52394 4L0.848215 5.67572C0.535796 5.98814 0.535796 6.49467 0.848215 6.80709C1.16063 7.11951 1.66717 7.11951 1.97959 6.80709L3.65531 5.13137L5.33122 6.80728C5.64364 7.1197 6.15017 7.1197 6.46259 6.80728C6.77501 6.49486 6.77501 5.98833 6.46259 5.67591L4.78668 4L6.46259 2.32409C6.77501 2.01167 6.77501 1.50514 6.46259 1.19272C6.15017 0.880297 5.64364 0.880297 5.33122 1.19272L3.65531 2.86863L1.97959 1.19291Z" fill="#F9FBFF"/>
|
||||
</svg>
|
After Width: | Height: | Size: 694 B |
3
app/images/icons/confirm.svg
Normal file
3
app/images/icons/confirm.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="7" height="5" viewBox="0 0 7 5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.97989 0.212475C6.27337 0.495775 6.27337 0.955095 5.97989 1.23839L2.16061 4.92513L0.220114 3.05198C-0.0733712 2.76868 -0.0733712 2.30936 0.220114 2.02606C0.513599 1.74276 0.989432 1.74276 1.28292 2.02606L2.16061 2.87329L4.91708 0.212475C5.21057 -0.070825 5.6864 -0.070825 5.97989 0.212475Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 456 B |
4
app/images/icons/error.svg
Normal file
4
app/images/icons/error.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="2" height="8" viewBox="0 0 2 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="2" height="5" rx="1" fill="white"/>
|
||||
<rect y="6" width="2" height="2" rx="1" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 203 B |
3
app/images/icons/new.svg
Executable file
3
app/images/icons/new.svg
Executable file
@ -0,0 +1,3 @@
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.76923 4.2003C1.3274 4.2003 0.969231 4.55847 0.969231 5.0003C0.969231 5.44213 1.3274 5.8003 1.76923 5.8003H4.20048V8.23077C4.20048 8.6726 4.55865 9.03077 5.00048 9.03077C5.44231 9.03077 5.80048 8.6726 5.80048 8.23077V5.8003H8.23077C8.6726 5.8003 9.03077 5.44213 9.03077 5.0003C9.03077 4.55847 8.6726 4.2003 8.23077 4.2003L5.80048 4.2003L5.80048 1.76923C5.80048 1.3274 5.44231 0.969229 5.00048 0.969229C4.55865 0.969229 4.20048 1.3274 4.20048 1.76923V4.2003H1.76923Z" fill="#F9FBFF"/>
|
||||
</svg>
|
After Width: | Height: | Size: 638 B |
7
app/images/icons/retry.svg
Executable file
7
app/images/icons/retry.svg
Executable file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 9.7 KiB |
3
app/images/icons/submitted.svg
Executable file
3
app/images/icons/submitted.svg
Executable file
@ -0,0 +1,3 @@
|
||||
<svg width="7" height="6" viewBox="0 0 7 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.24834 0.0498428C5.69016 0.0498428 6.04834 0.408016 6.04834 0.849844L6.04834 4.84984C6.04834 5.29167 5.69016 5.64984 5.24834 5.64984C4.80651 5.64984 4.44834 5.29167 4.44834 4.84984V2.68278L1.56558 5.56553C1.25316 5.87795 0.746632 5.87795 0.434212 5.56553C0.121793 5.25311 0.121793 4.74658 0.434212 4.43416L3.21853 1.64984L1.24834 1.64984C0.806507 1.64984 0.448335 1.29167 0.448335 0.849844C0.448335 0.408016 0.806507 0.0498428 1.24834 0.0498428L5.24834 0.0498428Z" fill="#F9FBFF"/>
|
||||
</svg>
|
After Width: | Height: | Size: 632 B |
@ -3,10 +3,12 @@ const TRANSACTION_TYPE_RETRY = 'retry'
|
||||
const TRANSACTION_TYPE_STANDARD = 'standard'
|
||||
|
||||
const TRANSACTION_STATUS_APPROVED = 'approved'
|
||||
const TRANSACTION_STATUS_CONFIRMED = 'confirmed'
|
||||
|
||||
module.exports = {
|
||||
TRANSACTION_TYPE_CANCEL,
|
||||
TRANSACTION_TYPE_RETRY,
|
||||
TRANSACTION_TYPE_STANDARD,
|
||||
TRANSACTION_STATUS_APPROVED,
|
||||
TRANSACTION_STATUS_CONFIRMED,
|
||||
}
|
||||
|
@ -230,13 +230,15 @@ class TransactionController extends EventEmitter {
|
||||
to allow the user to resign the transaction with a higher gas values
|
||||
@param originalTxId {number} - the id of the txMeta that
|
||||
you want to attempt to retry
|
||||
@param gasPrice {string=} - Optional gas price to be increased to use as the retry
|
||||
transaction's gas price
|
||||
@return {txMeta}
|
||||
*/
|
||||
|
||||
async retryTransaction (originalTxId) {
|
||||
async retryTransaction (originalTxId, gasPrice) {
|
||||
const originalTxMeta = this.txStateManager.getTx(originalTxId)
|
||||
const { txParams } = originalTxMeta
|
||||
const lastGasPrice = originalTxMeta.txParams.gasPrice
|
||||
const lastGasPrice = gasPrice || originalTxMeta.txParams.gasPrice
|
||||
const suggestedGasPriceBN = new ethUtil.BN(ethUtil.stripHexPrefix(this.getGasPrice()), 16)
|
||||
const lastGasPriceBN = new ethUtil.BN(ethUtil.stripHexPrefix(lastGasPrice), 16)
|
||||
// essentially lastGasPrice * 1.1 but
|
||||
|
@ -1144,8 +1144,8 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
* @param {string} txId - The ID of the transaction to speed up.
|
||||
* @param {Function} cb - The callback function called with a full state update.
|
||||
*/
|
||||
async retryTransaction (txId, cb) {
|
||||
await this.txController.retryTransaction(txId)
|
||||
async retryTransaction (txId, gasPrice, cb) {
|
||||
await this.txController.retryTransaction(txId, gasPrice)
|
||||
const state = await this.getState()
|
||||
return state
|
||||
}
|
||||
@ -1158,9 +1158,13 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
* @returns {object} MetaMask state
|
||||
*/
|
||||
async createCancelTransaction (originalTxId, customGasPrice, cb) {
|
||||
try {
|
||||
await this.txController.createCancelTransaction(originalTxId, customGasPrice)
|
||||
const state = await this.getState()
|
||||
return state
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async createSpeedUpTransaction (originalTxId, customGasPrice, cb) {
|
||||
|
@ -21,8 +21,8 @@ async function runConfirmSigRequestsTest (assert, done) {
|
||||
|
||||
const pendingRequestItem = $.find('.transaction-list-item .transaction-list-item__grid')
|
||||
|
||||
if (pendingRequestItem[0]) {
|
||||
pendingRequestItem[0].click()
|
||||
if (pendingRequestItem[2]) {
|
||||
pendingRequestItem[2].click()
|
||||
}
|
||||
|
||||
await timeout(1000)
|
||||
|
@ -30,35 +30,25 @@ async function runTxListItemsTest (assert, done) {
|
||||
metamaskLogo[0].click()
|
||||
|
||||
const txListItems = await queryAsync($, '.transaction-list-item')
|
||||
assert.equal(txListItems.length, 8, 'all tx list items are rendered')
|
||||
assert.equal(txListItems.length, 7, 'all tx list items are rendered')
|
||||
|
||||
const retryTxGrid = await findAsync($(txListItems[2]), '.transaction-list-item__grid')
|
||||
retryTxGrid[0].click()
|
||||
const retryTxDetails = await findAsync($, '.transaction-list-item-details')
|
||||
const headerButtons = await findAsync($(retryTxDetails[0]), '.transaction-list-item-details__header-button')
|
||||
assert.equal(headerButtons[0].textContent, 'speed up')
|
||||
|
||||
const approvedTx = txListItems[2]
|
||||
const approvedTx = txListItems[0]
|
||||
const approvedTxRenderedStatus = await findAsync($(approvedTx), '.transaction-list-item__status')
|
||||
assert.equal(approvedTxRenderedStatus[0].textContent, 'pending', 'approvedTx has correct label')
|
||||
|
||||
const unapprovedMsg = txListItems[0]
|
||||
const unapprovedMsg = txListItems[1]
|
||||
const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.transaction-list-item__action')
|
||||
assert.equal(unapprovedMsgDescription[0].textContent, 'Signature Request', 'unapprovedMsg has correct description')
|
||||
|
||||
const failedTx = txListItems[4]
|
||||
const failedTxRenderedStatus = await findAsync($(failedTx), '.transaction-list-item__status')
|
||||
assert.equal(failedTxRenderedStatus[0].textContent, 'Failed', 'failedTx has correct label')
|
||||
|
||||
const shapeShiftTx = txListItems[5]
|
||||
const shapeShiftTx = txListItems[4]
|
||||
const shapeShiftTxStatus = await findAsync($(shapeShiftTx), '.flex-column div:eq(1)')
|
||||
assert.equal(shapeShiftTxStatus[0].textContent, 'No deposits received', 'shapeShiftTx has correct status')
|
||||
|
||||
const rejectedTx = txListItems[5]
|
||||
const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.transaction-list-item__status')
|
||||
assert.equal(rejectedTxRenderedStatus[0].textContent, 'Rejected', 'rejectedTx has correct label')
|
||||
|
||||
const confirmedTokenTx = txListItems[6]
|
||||
const confirmedTokenTxAddress = await findAsync($(confirmedTokenTx), '.transaction-list-item__status')
|
||||
assert.equal(confirmedTokenTxAddress[0].textContent, 'Confirmed', 'confirmedTokenTx has correct address')
|
||||
|
||||
const rejectedTx = txListItems[7]
|
||||
const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.transaction-list-item__status')
|
||||
assert.equal(rejectedTxRenderedStatus[0].textContent, 'Rejected', 'rejectedTx has correct label')
|
||||
}
|
||||
|
@ -1793,13 +1793,13 @@ function markAccountsFound () {
|
||||
return callBackgroundThenUpdate(background.markAccountsFound)
|
||||
}
|
||||
|
||||
function retryTransaction (txId) {
|
||||
function retryTransaction (txId, gasPrice) {
|
||||
log.debug(`background.retryTransaction`)
|
||||
let newTxId
|
||||
|
||||
return (dispatch) => {
|
||||
return dispatch => {
|
||||
return new Promise((resolve, reject) => {
|
||||
background.retryTransaction(txId, (err, newState) => {
|
||||
background.retryTransaction(txId, gasPrice, (err, newState) => {
|
||||
if (err) {
|
||||
dispatch(actions.displayWarning(err.message))
|
||||
reject(err)
|
||||
|
@ -28,31 +28,29 @@ const mapStateToProps = (state, ownProps) => {
|
||||
transactionId,
|
||||
transactionStatus,
|
||||
originalGasPrice,
|
||||
defaultNewGasPrice,
|
||||
newGasFee,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
createCancelTransaction: txId => dispatch(createCancelTransaction(txId)),
|
||||
createCancelTransaction: (txId, customGasPrice) => {
|
||||
return dispatch(createCancelTransaction(txId, customGasPrice))
|
||||
},
|
||||
showTransactionConfirmedModal: () => dispatch(showModal({ name: 'TRANSACTION_CONFIRMED' })),
|
||||
}
|
||||
}
|
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
const { transactionId, ...restStateProps } = stateProps
|
||||
const {
|
||||
createCancelTransaction: dispatchCreateCancelTransaction,
|
||||
...restDispatchProps
|
||||
} = dispatchProps
|
||||
const { transactionId, defaultNewGasPrice, ...restStateProps } = stateProps
|
||||
const { createCancelTransaction, ...restDispatchProps } = dispatchProps
|
||||
|
||||
return {
|
||||
...restStateProps,
|
||||
...restDispatchProps,
|
||||
...ownProps,
|
||||
createCancelTransaction: newGasPrice => {
|
||||
return dispatchCreateCancelTransaction(transactionId, newGasPrice)
|
||||
},
|
||||
createCancelTransaction: () => createCancelTransaction(transactionId, defaultNewGasPrice),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -426,7 +426,7 @@ export default class ConfirmTransactionBase extends Component {
|
||||
toName={toName}
|
||||
toAddress={toAddress}
|
||||
showEdit={onEdit && !isTxReprice}
|
||||
action={action || name || this.context.t('unknownFunction')}
|
||||
action={action || name || this.context.t('contractInteraction')}
|
||||
title={title}
|
||||
titleComponent={this.renderTitleComponent()}
|
||||
subtitle={subtitle}
|
||||
|
@ -1,12 +1,13 @@
|
||||
.sender-to-recipient {
|
||||
&--default {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid $geyser;
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
|
||||
&--default {
|
||||
border-bottom: 1px solid $geyser;
|
||||
height: 42px;
|
||||
|
||||
.sender-to-recipient {
|
||||
@ -74,13 +75,6 @@
|
||||
}
|
||||
|
||||
&--cards {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
|
||||
.sender-to-recipient {
|
||||
&__party {
|
||||
display: flex;
|
||||
@ -117,4 +111,39 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--flat {
|
||||
.sender-to-recipient {
|
||||
&__party {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
min-width: 0;
|
||||
color: $dusty-gray;
|
||||
}
|
||||
|
||||
&__tooltip-wrapper {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: .6875rem;
|
||||
}
|
||||
|
||||
&__arrow-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,13 @@ import classnames from 'classnames'
|
||||
import Identicon from '../identicon'
|
||||
import Tooltip from '../tooltip-v2'
|
||||
import copyToClipboard from 'copy-to-clipboard'
|
||||
import { DEFAULT_VARIANT, CARDS_VARIANT } from './sender-to-recipient.constants'
|
||||
import { DEFAULT_VARIANT, CARDS_VARIANT, FLAT_VARIANT } from './sender-to-recipient.constants'
|
||||
import { checksumAddress } from '../../util'
|
||||
|
||||
const variantHash = {
|
||||
[DEFAULT_VARIANT]: 'sender-to-recipient--default',
|
||||
[CARDS_VARIANT]: 'sender-to-recipient--cards',
|
||||
[FLAT_VARIANT]: 'sender-to-recipient--flat',
|
||||
}
|
||||
|
||||
export default class SenderToRecipient extends PureComponent {
|
||||
@ -19,7 +20,7 @@ export default class SenderToRecipient extends PureComponent {
|
||||
recipientName: PropTypes.string,
|
||||
recipientAddress: PropTypes.string,
|
||||
t: PropTypes.func,
|
||||
variant: PropTypes.oneOf([DEFAULT_VARIANT, CARDS_VARIANT]),
|
||||
variant: PropTypes.oneOf([DEFAULT_VARIANT, CARDS_VARIANT, FLAT_VARIANT]),
|
||||
addressOnly: PropTypes.bool,
|
||||
assetImage: PropTypes.string,
|
||||
}
|
||||
@ -128,15 +129,8 @@ export default class SenderToRecipient extends PureComponent {
|
||||
}
|
||||
|
||||
renderArrow () {
|
||||
return this.props.variant === CARDS_VARIANT
|
||||
return this.props.variant === DEFAULT_VARIANT
|
||||
? (
|
||||
<div className="sender-to-recipient__arrow-container">
|
||||
<img
|
||||
height={20}
|
||||
src="./images/caret-right.svg"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="sender-to-recipient__arrow-container">
|
||||
<div className="sender-to-recipient__arrow-circle">
|
||||
<img
|
||||
@ -146,6 +140,13 @@ export default class SenderToRecipient extends PureComponent {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="sender-to-recipient__arrow-container">
|
||||
<img
|
||||
height={20}
|
||||
src="./images/caret-right.svg"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -154,7 +155,7 @@ export default class SenderToRecipient extends PureComponent {
|
||||
const checksummedSenderAddress = checksumAddress(senderAddress)
|
||||
|
||||
return (
|
||||
<div className={classnames(variantHash[variant])}>
|
||||
<div className={classnames('sender-to-recipient', variantHash[variant])}>
|
||||
<div
|
||||
className={classnames('sender-to-recipient__party sender-to-recipient__party--sender')}
|
||||
onClick={() => {
|
||||
|
@ -1,3 +1,4 @@
|
||||
// Component design variants
|
||||
export const DEFAULT_VARIANT = 'DEFAULT_VARIANT'
|
||||
export const CARDS_VARIANT = 'CARDS_VARIANT'
|
||||
export const FLAT_VARIANT = 'FLAT_VARIANT'
|
||||
|
@ -1,7 +1,8 @@
|
||||
.transaction-activity-log {
|
||||
&__card {
|
||||
background: $white;
|
||||
height: 100%;
|
||||
&__title {
|
||||
border-bottom: 1px solid #d8d8d8;
|
||||
padding-bottom: 4px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
&__activities-container {
|
||||
@ -21,8 +22,8 @@
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 6px;
|
||||
border-right: 1px solid $scorpion;
|
||||
width: 7px;
|
||||
border-right: 1px solid #909090;
|
||||
}
|
||||
|
||||
&:first-child::after {
|
||||
@ -40,22 +41,25 @@
|
||||
}
|
||||
|
||||
&__activity-icon {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin-right: 6px;
|
||||
border-radius: 50%;
|
||||
background: $scorpion;
|
||||
background: #909090;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__activity-text {
|
||||
color: $scorpion;
|
||||
color: $dusty-gray;
|
||||
font-size: .75rem;
|
||||
cursor: pointer;
|
||||
|
||||
@media screen and (min-width: $break-large) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
&:hover {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,6 +68,16 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__entry-container {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__action-link {
|
||||
font-size: .75rem;
|
||||
cursor: pointer;
|
||||
color: $curious-blue;
|
||||
}
|
||||
|
||||
b {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
@ -2,34 +2,100 @@ import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import TransactionActivityLog from '../transaction-activity-log.component'
|
||||
import Card from '../../card'
|
||||
|
||||
describe('TransactionActivityLog Component', () => {
|
||||
it('should render properly', () => {
|
||||
const transaction = {
|
||||
history: [],
|
||||
id: 1,
|
||||
status: 'confirmed',
|
||||
txParams: {
|
||||
from: '0x1',
|
||||
gas: '0x5208',
|
||||
gasPrice: '0x3b9aca00',
|
||||
nonce: '0xa4',
|
||||
to: '0x2',
|
||||
const activities = [
|
||||
{
|
||||
eventKey: 'transactionCreated',
|
||||
hash: '0xe46c7f9b39af2fbf1c53e66f72f80343ab54c2c6dba902d51fb98ada08fe1a63',
|
||||
id: 2005383477493174,
|
||||
timestamp: 1543957986150,
|
||||
value: '0x2386f26fc10000',
|
||||
}, {
|
||||
eventKey: 'transactionSubmitted',
|
||||
hash: '0xe46c7f9b39af2fbf1c53e66f72f80343ab54c2c6dba902d51fb98ada08fe1a63',
|
||||
id: 2005383477493174,
|
||||
timestamp: 1543957987853,
|
||||
value: '0x1319718a5000',
|
||||
}, {
|
||||
eventKey: 'transactionResubmitted',
|
||||
hash: '0x7d09d337fc6f5d6fe2dbf3a6988d69532deb0a82b665f9180b5a20db377eea87',
|
||||
id: 2005383477493175,
|
||||
timestamp: 1543957991563,
|
||||
value: '0x1502634b5800',
|
||||
}, {
|
||||
eventKey: 'transactionConfirmed',
|
||||
hash: '0x7d09d337fc6f5d6fe2dbf3a6988d69532deb0a82b665f9180b5a20db377eea87',
|
||||
id: 2005383477493175,
|
||||
timestamp: 1543958029960,
|
||||
value: '0x1502634b5800',
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
const wrapper = shallow(
|
||||
<TransactionActivityLog
|
||||
transaction={transaction}
|
||||
activities={activities}
|
||||
className="test-class"
|
||||
inlineRetryIndex={-1}
|
||||
inlineCancelIndex={-1}
|
||||
nativeCurrency="ETH"
|
||||
onCancel={() => {}}
|
||||
onRetry={() => {}}
|
||||
primaryTransactionStatus="confirmed"
|
||||
/>,
|
||||
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
|
||||
)
|
||||
|
||||
assert.ok(wrapper.hasClass('transaction-activity-log'))
|
||||
assert.ok(wrapper.hasClass('test-class'))
|
||||
assert.equal(wrapper.find(Card).length, 1)
|
||||
})
|
||||
|
||||
it('should render inline retry and cancel buttons', () => {
|
||||
const activities = [
|
||||
{
|
||||
eventKey: 'transactionCreated',
|
||||
hash: '0xa',
|
||||
id: 1,
|
||||
timestamp: 1,
|
||||
value: '0x1',
|
||||
}, {
|
||||
eventKey: 'transactionSubmitted',
|
||||
hash: '0xa',
|
||||
id: 1,
|
||||
timestamp: 2,
|
||||
value: '0x1',
|
||||
}, {
|
||||
eventKey: 'transactionResubmitted',
|
||||
hash: '0x7d09d337fc6f5d6fe2dbf3a6988d69532deb0a82b665f9180b5a20db377eea87',
|
||||
id: 2,
|
||||
timestamp: 3,
|
||||
value: '0x1',
|
||||
}, {
|
||||
eventKey: 'transactionCancelAttempted',
|
||||
hash: '0x7d09d337fc6f5d6fe2dbf3a6988d69532deb0a82b665f9180b5a20db377eea87',
|
||||
id: 3,
|
||||
timestamp: 4,
|
||||
value: '0x1',
|
||||
},
|
||||
]
|
||||
|
||||
const wrapper = shallow(
|
||||
<TransactionActivityLog
|
||||
activities={activities}
|
||||
className="test-class"
|
||||
inlineRetryIndex={2}
|
||||
inlineCancelIndex={3}
|
||||
nativeCurrency="ETH"
|
||||
onCancel={() => {}}
|
||||
onRetry={() => {}}
|
||||
primaryTransactionStatus="pending"
|
||||
/>,
|
||||
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
|
||||
)
|
||||
|
||||
assert.ok(wrapper.hasClass('transaction-activity-log'))
|
||||
assert.ok(wrapper.hasClass('test-class'))
|
||||
assert.equal(wrapper.find('.transaction-activity-log__action-link').length, 2)
|
||||
})
|
||||
})
|
||||
|
@ -1,5 +1,130 @@
|
||||
import assert from 'assert'
|
||||
import { getActivities } from '../transaction-activity-log.util'
|
||||
import { combineTransactionHistories, getActivities } from '../transaction-activity-log.util'
|
||||
|
||||
describe('combineTransactionHistories', () => {
|
||||
it('should return no activites for an empty list of transactions', () => {
|
||||
assert.deepEqual(combineTransactionHistories([]), [])
|
||||
})
|
||||
|
||||
it('should return activities for an array of transactions', () => {
|
||||
const transactions = [
|
||||
{
|
||||
estimatedGas: '0x5208',
|
||||
hash: '0xa14f13d36b3901e352ce3a7acb9b47b001e5a3370f06232a0953c6fc6fad91b3',
|
||||
history: [
|
||||
{
|
||||
'id': 6400627574331058,
|
||||
'time': 1543958845581,
|
||||
'status': 'unapproved',
|
||||
'metamaskNetworkId': '3',
|
||||
'loadingDefaults': true,
|
||||
'txParams': {
|
||||
'from': '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
|
||||
'to': '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
|
||||
'value': '0x2386f26fc10000',
|
||||
'gas': '0x5208',
|
||||
'gasPrice': '0x3b9aca00',
|
||||
},
|
||||
'type': 'standard',
|
||||
},
|
||||
[{ 'op': 'replace', 'path': '/status', 'value': 'approved', 'note': 'txStateManager: setting status to approved', 'timestamp': 1543958847813 }],
|
||||
[{ 'op': 'replace', 'path': '/status', 'value': 'submitted', 'note': 'txStateManager: setting status to submitted', 'timestamp': 1543958848147 }],
|
||||
[{ 'op': 'replace', 'path': '/status', 'value': 'dropped', 'note': 'txStateManager: setting status to dropped', 'timestamp': 1543958897181 }, { 'op': 'add', 'path': '/replacedBy', 'value': '0xecbe181ee67c4291d04a7cb9ffbf1d5d831e4fbaa89994fd06bab5dd4cc79b33' }],
|
||||
],
|
||||
id: 6400627574331058,
|
||||
loadingDefaults: false,
|
||||
metamaskNetworkId: '3',
|
||||
status: 'dropped',
|
||||
submittedTime: 1543958848135,
|
||||
time: 1543958845581,
|
||||
txParams: {
|
||||
from: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
|
||||
gas: '0x5208',
|
||||
gasPrice: '0x3b9aca00',
|
||||
nonce: '0x32',
|
||||
to: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
|
||||
value: '0x2386f26fc10000',
|
||||
},
|
||||
type: 'standard',
|
||||
}, {
|
||||
hash: '0xecbe181ee67c4291d04a7cb9ffbf1d5d831e4fbaa89994fd06bab5dd4cc79b33',
|
||||
history: [
|
||||
{
|
||||
'id': 6400627574331060,
|
||||
'time': 1543958857697,
|
||||
'status': 'unapproved',
|
||||
'metamaskNetworkId': '3',
|
||||
'loadingDefaults': false,
|
||||
'txParams': {
|
||||
'from': '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
|
||||
'to': '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
|
||||
'value': '0x2386f26fc10000',
|
||||
'gas': '0x5208',
|
||||
'gasPrice': '0x3b9aca00',
|
||||
'nonce': '0x32',
|
||||
},
|
||||
'lastGasPrice': '0x4190ab00',
|
||||
'type': 'retry',
|
||||
},
|
||||
[{ 'op': 'replace', 'path': '/txParams/gasPrice', 'value': '0x481f2280', 'note': 'confTx: user approved transaction', 'timestamp': 1543958859470 }],
|
||||
[{ 'op': 'replace', 'path': '/status', 'value': 'approved', 'note': 'txStateManager: setting status to approved', 'timestamp': 1543958859485 }],
|
||||
[{ 'op': 'replace', 'path': '/status', 'value': 'signed', 'note': 'transactions#publishTransaction', 'timestamp': 1543958859889 }],
|
||||
[{ 'op': 'replace', 'path': '/status', 'value': 'submitted', 'note': 'txStateManager: setting status to submitted', 'timestamp': 1543958860061 }], [{ 'op': 'add', 'path': '/firstRetryBlockNumber', 'value': '0x45a0fd', 'note': 'transactions/pending-tx-tracker#event: tx:block-update', 'timestamp': 1543958896466 }],
|
||||
[{ 'op': 'replace', 'path': '/status', 'value': 'confirmed', 'timestamp': 1543958897165 }],
|
||||
],
|
||||
id: 6400627574331060,
|
||||
lastGasPrice: '0x4190ab00',
|
||||
loadingDefaults: false,
|
||||
metamaskNetworkId: '3',
|
||||
status: 'confirmed',
|
||||
submittedTime: 1543958860054,
|
||||
time: 1543958857697,
|
||||
txParams: {
|
||||
from: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
|
||||
gas: '0x5208',
|
||||
gasPrice: '0x481f2280',
|
||||
nonce: '0x32',
|
||||
to: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
|
||||
value: '0x2386f26fc10000',
|
||||
},
|
||||
txReceipt: {
|
||||
status: '0x1',
|
||||
},
|
||||
type: 'retry',
|
||||
},
|
||||
]
|
||||
|
||||
const expected = [
|
||||
{
|
||||
id: 6400627574331058,
|
||||
hash: '0xa14f13d36b3901e352ce3a7acb9b47b001e5a3370f06232a0953c6fc6fad91b3',
|
||||
eventKey: 'transactionCreated',
|
||||
timestamp: 1543958845581,
|
||||
value: '0x2386f26fc10000',
|
||||
}, {
|
||||
id: 6400627574331058,
|
||||
hash: '0xa14f13d36b3901e352ce3a7acb9b47b001e5a3370f06232a0953c6fc6fad91b3',
|
||||
eventKey: 'transactionSubmitted',
|
||||
timestamp: 1543958848147,
|
||||
value: '0x1319718a5000',
|
||||
}, {
|
||||
id: 6400627574331060,
|
||||
hash: '0xecbe181ee67c4291d04a7cb9ffbf1d5d831e4fbaa89994fd06bab5dd4cc79b33',
|
||||
eventKey: 'transactionResubmitted',
|
||||
timestamp: 1543958860061,
|
||||
value: '0x171c3a061400',
|
||||
}, {
|
||||
id: 6400627574331060,
|
||||
hash: '0xecbe181ee67c4291d04a7cb9ffbf1d5d831e4fbaa89994fd06bab5dd4cc79b33',
|
||||
eventKey: 'transactionConfirmed',
|
||||
timestamp: 1543958897165,
|
||||
value: '0x171c3a061400',
|
||||
},
|
||||
]
|
||||
|
||||
assert.deepEqual(combineTransactionHistories(transactions), expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getActivities', () => {
|
||||
it('should return no activities for an empty history', () => {
|
||||
@ -178,6 +303,7 @@ describe('getActivities', () => {
|
||||
to: '0x2',
|
||||
value: '0x2386f26fc10000',
|
||||
},
|
||||
hash: '0xabc',
|
||||
}
|
||||
|
||||
const expectedResult = [
|
||||
@ -185,24 +311,25 @@ describe('getActivities', () => {
|
||||
'eventKey': 'transactionCreated',
|
||||
'timestamp': 1535507561452,
|
||||
'value': '0x2386f26fc10000',
|
||||
},
|
||||
{
|
||||
'eventKey': 'transactionUpdatedGas',
|
||||
'timestamp': 1535664571504,
|
||||
'value': '0x77359400',
|
||||
'id': 1,
|
||||
'hash': '0xabc',
|
||||
},
|
||||
{
|
||||
'eventKey': 'transactionSubmitted',
|
||||
'timestamp': 1535507564665,
|
||||
'value': undefined,
|
||||
'value': '0x2632e314a000',
|
||||
'id': 1,
|
||||
'hash': '0xabc',
|
||||
},
|
||||
{
|
||||
'eventKey': 'transactionConfirmed',
|
||||
'timestamp': 1535507615993,
|
||||
'value': undefined,
|
||||
'value': '0x2632e314a000',
|
||||
'id': 1,
|
||||
'hash': '0xabc',
|
||||
},
|
||||
]
|
||||
|
||||
assert.deepEqual(getActivities(transaction), expectedResult)
|
||||
assert.deepEqual(getActivities(transaction, true), expectedResult)
|
||||
})
|
||||
})
|
||||
|
@ -0,0 +1 @@
|
||||
export { default } from './transaction-activity-log-icon.component'
|
@ -0,0 +1,55 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import {
|
||||
TRANSACTION_CREATED_EVENT,
|
||||
TRANSACTION_SUBMITTED_EVENT,
|
||||
TRANSACTION_RESUBMITTED_EVENT,
|
||||
TRANSACTION_CONFIRMED_EVENT,
|
||||
TRANSACTION_DROPPED_EVENT,
|
||||
TRANSACTION_ERRORED_EVENT,
|
||||
TRANSACTION_CANCEL_ATTEMPTED_EVENT,
|
||||
TRANSACTION_CANCEL_SUCCESS_EVENT,
|
||||
} from '../transaction-activity-log.constants'
|
||||
|
||||
const imageHash = {
|
||||
[TRANSACTION_CREATED_EVENT]: '/images/icons/new.svg',
|
||||
[TRANSACTION_SUBMITTED_EVENT]: '/images/icons/submitted.svg',
|
||||
[TRANSACTION_RESUBMITTED_EVENT]: '/images/icons/retry.svg',
|
||||
[TRANSACTION_CONFIRMED_EVENT]: '/images/icons/confirm.svg',
|
||||
[TRANSACTION_DROPPED_EVENT]: '/images/icons/cancelled.svg',
|
||||
[TRANSACTION_ERRORED_EVENT]: '/images/icons/error.svg',
|
||||
[TRANSACTION_CANCEL_ATTEMPTED_EVENT]: '/images/icons/cancelled.svg',
|
||||
[TRANSACTION_CANCEL_SUCCESS_EVENT]: '/images/icons/cancelled.svg',
|
||||
}
|
||||
|
||||
export default class TransactionActivityLogIcon extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
eventKey: PropTypes.oneOf(Object.keys(imageHash)),
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, eventKey } = this.props
|
||||
const imagePath = imageHash[eventKey]
|
||||
|
||||
return (
|
||||
<div className={classnames('transaction-activity-log-icon', className)}>
|
||||
{
|
||||
imagePath && (
|
||||
<img
|
||||
src={imagePath}
|
||||
height={9}
|
||||
width={9}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import { getActivities } from './transaction-activity-log.util'
|
||||
import Card from '../card'
|
||||
import { getEthConversionFromWeiHex, getValueFromWeiHex } from '../../helpers/conversions.util'
|
||||
import { formatDate } from '../../util'
|
||||
import TransactionActivityLogIcon from './transaction-activity-log-icon'
|
||||
import { CONFIRMED_STATUS } from './transaction-activity-log.constants'
|
||||
import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
|
||||
|
||||
export default class TransactionActivityLog extends PureComponent {
|
||||
static contextTypes = {
|
||||
@ -12,41 +13,64 @@ export default class TransactionActivityLog extends PureComponent {
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
transaction: PropTypes.object,
|
||||
activities: PropTypes.array,
|
||||
className: PropTypes.string,
|
||||
conversionRate: PropTypes.number,
|
||||
inlineRetryIndex: PropTypes.number,
|
||||
inlineCancelIndex: PropTypes.number,
|
||||
nativeCurrency: PropTypes.string,
|
||||
onCancel: PropTypes.func,
|
||||
onRetry: PropTypes.func,
|
||||
primaryTransaction: PropTypes.object,
|
||||
}
|
||||
|
||||
state = {
|
||||
activities: [],
|
||||
handleActivityClick = hash => {
|
||||
const { primaryTransaction } = this.props
|
||||
const { metamaskNetworkId } = primaryTransaction
|
||||
|
||||
const prefix = prefixForNetwork(metamaskNetworkId)
|
||||
const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
|
||||
|
||||
global.platform.openWindow({ url: etherscanUrl })
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.setActivites()
|
||||
renderInlineRetry (index, activity) {
|
||||
const { t } = this.context
|
||||
const { inlineRetryIndex, primaryTransaction = {}, onRetry } = this.props
|
||||
const { status } = primaryTransaction
|
||||
const { id } = activity
|
||||
|
||||
return status !== CONFIRMED_STATUS && index === inlineRetryIndex
|
||||
? (
|
||||
<div
|
||||
className="transaction-activity-log__action-link"
|
||||
onClick={() => onRetry(id)}
|
||||
>
|
||||
{ t('speedUpTransaction') }
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const {
|
||||
transaction: { history: prevHistory = [], txReceipt: { status: prevStatus } = {} } = {},
|
||||
} = prevProps
|
||||
const {
|
||||
transaction: { history = [], txReceipt: { status } = {} } = {},
|
||||
} = this.props
|
||||
renderInlineCancel (index, activity) {
|
||||
const { t } = this.context
|
||||
const { inlineCancelIndex, primaryTransaction = {}, onCancel } = this.props
|
||||
const { status } = primaryTransaction
|
||||
const { id } = activity
|
||||
|
||||
if (prevHistory.length !== history.length || prevStatus !== status) {
|
||||
this.setActivites()
|
||||
}
|
||||
}
|
||||
|
||||
setActivites () {
|
||||
const activities = getActivities(this.props.transaction)
|
||||
this.setState({ activities })
|
||||
return status !== CONFIRMED_STATUS && index === inlineCancelIndex
|
||||
? (
|
||||
<div
|
||||
className="transaction-activity-log__action-link"
|
||||
onClick={() => onCancel(id)}
|
||||
>
|
||||
{ t('speedUpCancellation') }
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
renderActivity (activity, index) {
|
||||
const { conversionRate, nativeCurrency } = this.props
|
||||
const { eventKey, value, timestamp } = activity
|
||||
const { eventKey, value, timestamp, hash } = activity
|
||||
const ethValue = index === 0
|
||||
? `${getValueFromWeiHex({
|
||||
value,
|
||||
@ -55,8 +79,13 @@ export default class TransactionActivityLog extends PureComponent {
|
||||
conversionRate,
|
||||
numberOfDecimals: 6,
|
||||
})} ${nativeCurrency}`
|
||||
: getEthConversionFromWeiHex({ value, fromCurrency: nativeCurrency, conversionRate })
|
||||
const formattedTimestamp = formatDate(timestamp)
|
||||
: getEthConversionFromWeiHex({
|
||||
value,
|
||||
fromCurrency: nativeCurrency,
|
||||
conversionRate,
|
||||
numberOfDecimals: 3,
|
||||
})
|
||||
const formattedTimestamp = formatDate(timestamp, '14:30 on 3/16/2014')
|
||||
const activityText = this.context.t(eventKey, [ethValue, formattedTimestamp])
|
||||
|
||||
return (
|
||||
@ -64,32 +93,37 @@ export default class TransactionActivityLog extends PureComponent {
|
||||
key={index}
|
||||
className="transaction-activity-log__activity"
|
||||
>
|
||||
<div className="transaction-activity-log__activity-icon" />
|
||||
<TransactionActivityLogIcon
|
||||
className="transaction-activity-log__activity-icon"
|
||||
eventKey={eventKey}
|
||||
/>
|
||||
<div className="transaction-activity-log__entry-container">
|
||||
<div
|
||||
className="transaction-activity-log__activity-text"
|
||||
title={activityText}
|
||||
onClick={() => this.handleActivityClick(hash)}
|
||||
>
|
||||
{ activityText }
|
||||
</div>
|
||||
{ this.renderInlineRetry(index, activity) }
|
||||
{ this.renderInlineCancel(index, activity) }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
const { className } = this.props
|
||||
const { activities } = this.state
|
||||
const { className, activities } = this.props
|
||||
|
||||
return (
|
||||
<div className={classnames('transaction-activity-log', className)}>
|
||||
<Card
|
||||
title={t('activityLog')}
|
||||
className="transaction-activity-log__card"
|
||||
>
|
||||
<div className="transaction-activity-log__title">
|
||||
{ t('activityLog') }
|
||||
</div>
|
||||
<div className="transaction-activity-log__activities-container">
|
||||
{ activities.map((activity, index) => this.renderActivity(activity, index)) }
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
export const TRANSACTION_CREATED_EVENT = 'transactionCreated'
|
||||
export const TRANSACTION_SUBMITTED_EVENT = 'transactionSubmitted'
|
||||
export const TRANSACTION_RESUBMITTED_EVENT = 'transactionResubmitted'
|
||||
export const TRANSACTION_CONFIRMED_EVENT = 'transactionConfirmed'
|
||||
export const TRANSACTION_DROPPED_EVENT = 'transactionDropped'
|
||||
export const TRANSACTION_UPDATED_EVENT = 'transactionUpdated'
|
||||
export const TRANSACTION_ERRORED_EVENT = 'transactionErrored'
|
||||
export const TRANSACTION_CANCEL_ATTEMPTED_EVENT = 'transactionCancelAttempted'
|
||||
export const TRANSACTION_CANCEL_SUCCESS_EVENT = 'transactionCancelSuccess'
|
||||
|
||||
export const SUBMITTED_STATUS = 'submitted'
|
||||
export const CONFIRMED_STATUS = 'confirmed'
|
||||
export const DROPPED_STATUS = 'dropped'
|
@ -1,6 +1,14 @@
|
||||
import { connect } from 'react-redux'
|
||||
import R from 'ramda'
|
||||
import TransactionActivityLog from './transaction-activity-log.component'
|
||||
import { conversionRateSelector, getNativeCurrency } from '../../selectors'
|
||||
import { combineTransactionHistories } from './transaction-activity-log.util'
|
||||
import {
|
||||
TRANSACTION_RESUBMITTED_EVENT,
|
||||
TRANSACTION_CANCEL_ATTEMPTED_EVENT,
|
||||
} from './transaction-activity-log.constants'
|
||||
|
||||
const matchesEventKey = matchEventKey => ({ eventKey }) => eventKey === matchEventKey
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
@ -9,4 +17,28 @@ const mapStateToProps = state => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(TransactionActivityLog)
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
const {
|
||||
transactionGroup: {
|
||||
transactions = [],
|
||||
primaryTransaction,
|
||||
} = {},
|
||||
...restOwnProps
|
||||
} = ownProps
|
||||
|
||||
const activities = combineTransactionHistories(transactions)
|
||||
const inlineRetryIndex = R.findLastIndex(matchesEventKey(TRANSACTION_RESUBMITTED_EVENT))(activities)
|
||||
const inlineCancelIndex = R.findLastIndex(matchesEventKey(TRANSACTION_CANCEL_ATTEMPTED_EVENT))(activities)
|
||||
|
||||
return {
|
||||
...stateProps,
|
||||
...dispatchProps,
|
||||
...restOwnProps,
|
||||
activities,
|
||||
inlineRetryIndex,
|
||||
inlineCancelIndex,
|
||||
primaryTransaction,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null, mergeProps)(TransactionActivityLog)
|
||||
|
@ -1,28 +1,39 @@
|
||||
import { getHexGasTotal } from '../../helpers/confirm-transaction/util'
|
||||
|
||||
// path constants
|
||||
const STATUS_PATH = '/status'
|
||||
const GAS_PRICE_PATH = '/txParams/gasPrice'
|
||||
|
||||
// status constants
|
||||
const UNAPPROVED_STATUS = 'unapproved'
|
||||
const SUBMITTED_STATUS = 'submitted'
|
||||
const CONFIRMED_STATUS = 'confirmed'
|
||||
const DROPPED_STATUS = 'dropped'
|
||||
const GAS_LIMIT_PATH = '/txParams/gas'
|
||||
|
||||
// op constants
|
||||
const REPLACE_OP = 'replace'
|
||||
|
||||
// event constants
|
||||
const TRANSACTION_CREATED_EVENT = 'transactionCreated'
|
||||
const TRANSACTION_UPDATED_GAS_EVENT = 'transactionUpdatedGas'
|
||||
const TRANSACTION_SUBMITTED_EVENT = 'transactionSubmitted'
|
||||
const TRANSACTION_CONFIRMED_EVENT = 'transactionConfirmed'
|
||||
const TRANSACTION_DROPPED_EVENT = 'transactionDropped'
|
||||
const TRANSACTION_UPDATED_EVENT = 'transactionUpdated'
|
||||
const TRANSACTION_ERRORED_EVENT = 'transactionErrored'
|
||||
import {
|
||||
// event constants
|
||||
TRANSACTION_CREATED_EVENT,
|
||||
TRANSACTION_SUBMITTED_EVENT,
|
||||
TRANSACTION_RESUBMITTED_EVENT,
|
||||
TRANSACTION_CONFIRMED_EVENT,
|
||||
TRANSACTION_DROPPED_EVENT,
|
||||
TRANSACTION_UPDATED_EVENT,
|
||||
TRANSACTION_ERRORED_EVENT,
|
||||
TRANSACTION_CANCEL_ATTEMPTED_EVENT,
|
||||
TRANSACTION_CANCEL_SUCCESS_EVENT,
|
||||
// status constants
|
||||
SUBMITTED_STATUS,
|
||||
CONFIRMED_STATUS,
|
||||
DROPPED_STATUS,
|
||||
} from './transaction-activity-log.constants'
|
||||
|
||||
import {
|
||||
TRANSACTION_TYPE_CANCEL,
|
||||
TRANSACTION_TYPE_RETRY,
|
||||
} from '../../../../app/scripts/controllers/transactions/enums'
|
||||
|
||||
const eventPathsHash = {
|
||||
[STATUS_PATH]: true,
|
||||
[GAS_PRICE_PATH]: true,
|
||||
[GAS_LIMIT_PATH]: true,
|
||||
}
|
||||
|
||||
const statusHash = {
|
||||
@ -31,22 +42,39 @@ const statusHash = {
|
||||
[DROPPED_STATUS]: TRANSACTION_DROPPED_EVENT,
|
||||
}
|
||||
|
||||
function eventCreator (eventKey, timestamp, value) {
|
||||
return {
|
||||
eventKey,
|
||||
/**
|
||||
* @name getActivities
|
||||
* @param {Object} transaction - txMeta object
|
||||
* @param {boolean} isFirstTransaction - True if the transaction is the first created transaction
|
||||
* in the list of transactions with the same nonce. If so, we use this transaction to create the
|
||||
* transactionCreated activity.
|
||||
* @returns {Array}
|
||||
*/
|
||||
export function getActivities (transaction, isFirstTransaction = false) {
|
||||
const { id, hash, history = [], txReceipt: { status } = {}, type } = transaction
|
||||
|
||||
let cachedGasLimit = '0x0'
|
||||
let cachedGasPrice = '0x0'
|
||||
|
||||
const historyActivities = history.reduce((acc, base, index) => {
|
||||
// First history item should be transaction creation
|
||||
if (index === 0 && !Array.isArray(base) && base.txParams) {
|
||||
const { time: timestamp, txParams: { value, gas = '0x0', gasPrice = '0x0' } = {} } = base
|
||||
// The cached gas limit and gas price are used to display the gas fee in the activity log. We
|
||||
// need to cache these values because the status update history events don't provide us with
|
||||
// the latest gas limit and gas price.
|
||||
cachedGasLimit = gas
|
||||
cachedGasPrice = gasPrice
|
||||
|
||||
if (isFirstTransaction) {
|
||||
return acc.concat({
|
||||
id,
|
||||
hash,
|
||||
eventKey: TRANSACTION_CREATED_EVENT,
|
||||
timestamp,
|
||||
value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function getActivities (transaction) {
|
||||
const { history = [], txReceipt: { status } = {} } = transaction
|
||||
|
||||
const historyActivities = history.reduce((acc, base) => {
|
||||
// First history item should be transaction creation
|
||||
if (!Array.isArray(base) && base.status === UNAPPROVED_STATUS && base.txParams) {
|
||||
const { time, txParams: { value } = {} } = base
|
||||
return acc.concat(eventCreator(TRANSACTION_CREATED_EVENT, time, value))
|
||||
// An entry in the history may be an array of more sub-entries.
|
||||
} else if (Array.isArray(base)) {
|
||||
const events = []
|
||||
@ -60,20 +88,69 @@ export function getActivities (transaction) {
|
||||
if (path in eventPathsHash && op === REPLACE_OP) {
|
||||
switch (path) {
|
||||
case STATUS_PATH: {
|
||||
const gasFee = getHexGasTotal({ gasLimit: cachedGasLimit, gasPrice: cachedGasPrice })
|
||||
|
||||
if (value in statusHash) {
|
||||
events.push(eventCreator(statusHash[value], timestamp))
|
||||
let eventKey = statusHash[value]
|
||||
|
||||
// If the status is 'submitted', we need to determine whether the event is a
|
||||
// transaction retry or a cancellation attempt.
|
||||
if (value === SUBMITTED_STATUS) {
|
||||
if (type === TRANSACTION_TYPE_RETRY) {
|
||||
eventKey = TRANSACTION_RESUBMITTED_EVENT
|
||||
} else if (type === TRANSACTION_TYPE_CANCEL) {
|
||||
eventKey = TRANSACTION_CANCEL_ATTEMPTED_EVENT
|
||||
}
|
||||
} else if (value === CONFIRMED_STATUS) {
|
||||
if (type === TRANSACTION_TYPE_CANCEL) {
|
||||
eventKey = TRANSACTION_CANCEL_SUCCESS_EVENT
|
||||
}
|
||||
}
|
||||
|
||||
events.push({
|
||||
id,
|
||||
hash,
|
||||
eventKey,
|
||||
timestamp,
|
||||
value: gasFee,
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case GAS_PRICE_PATH: {
|
||||
events.push(eventCreator(TRANSACTION_UPDATED_GAS_EVENT, timestamp, value))
|
||||
// If the gas price or gas limit has been changed, we update the gasFee of the
|
||||
// previously submitted event. These events happen when the gas limit and gas price is
|
||||
// changed at the confirm screen.
|
||||
case GAS_PRICE_PATH:
|
||||
case GAS_LIMIT_PATH: {
|
||||
const lastEvent = events[events.length - 1] || {}
|
||||
const { lastEventKey } = lastEvent
|
||||
|
||||
if (path === GAS_LIMIT_PATH) {
|
||||
cachedGasLimit = value
|
||||
} else if (path === GAS_PRICE_PATH) {
|
||||
cachedGasPrice = value
|
||||
}
|
||||
|
||||
if (lastEventKey === TRANSACTION_SUBMITTED_EVENT ||
|
||||
lastEventKey === TRANSACTION_RESUBMITTED_EVENT) {
|
||||
lastEvent.value = getHexGasTotal({
|
||||
gasLimit: cachedGasLimit,
|
||||
gasPrice: cachedGasPrice,
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
events.push(eventCreator(TRANSACTION_UPDATED_EVENT, timestamp))
|
||||
events.push({
|
||||
id,
|
||||
hash,
|
||||
eventKey: TRANSACTION_UPDATED_EVENT,
|
||||
timestamp,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -88,6 +165,60 @@ export function getActivities (transaction) {
|
||||
// If txReceipt.status is '0x0', that means that an on-chain error occured for the transaction,
|
||||
// so we add an error entry to the Activity Log.
|
||||
return status === '0x0'
|
||||
? historyActivities.concat(eventCreator(TRANSACTION_ERRORED_EVENT))
|
||||
? historyActivities.concat({ id, hash, eventKey: TRANSACTION_ERRORED_EVENT })
|
||||
: historyActivities
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Removes "Transaction dropped" activities from a list of sorted activities if one of
|
||||
* the transactions has been confirmed. Typically, if multiple transactions have the same nonce,
|
||||
* once one transaction is confirmed, the rest are dropped. In this case, we don't want to show
|
||||
* multiple "Transaction dropped" activities, and instead want to show a single "Transaction
|
||||
* confirmed".
|
||||
* @param {Array} activities - List of sorted activities generated from the getActivities function.
|
||||
* @returns {Array}
|
||||
*/
|
||||
function filterSortedActivities (activities) {
|
||||
const filteredActivities = []
|
||||
const hasConfirmedActivity = Boolean(activities.find(({ eventKey }) => (
|
||||
eventKey === TRANSACTION_CONFIRMED_EVENT || eventKey === TRANSACTION_CANCEL_SUCCESS_EVENT
|
||||
)))
|
||||
let addedDroppedActivity = false
|
||||
|
||||
activities.forEach(activity => {
|
||||
if (activity.eventKey === TRANSACTION_DROPPED_EVENT) {
|
||||
if (!hasConfirmedActivity && !addedDroppedActivity) {
|
||||
filteredActivities.push(activity)
|
||||
addedDroppedActivity = true
|
||||
}
|
||||
} else {
|
||||
filteredActivities.push(activity)
|
||||
}
|
||||
})
|
||||
|
||||
return filteredActivities
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines the histories of an array of transactions into a single array.
|
||||
* @param {Array} transactions - Array of txMeta transaction objects.
|
||||
* @returns {Array}
|
||||
*/
|
||||
export function combineTransactionHistories (transactions = []) {
|
||||
if (!transactions.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const activities = []
|
||||
|
||||
transactions.forEach((transaction, index) => {
|
||||
// The first transaction should be the transaction with the earliest submittedTime. We show the
|
||||
// 'created' and 'submitted' activities here. All subsequent transactions will use 'resubmitted'
|
||||
// instead.
|
||||
const transactionActivities = getActivities(transaction, index === 0)
|
||||
activities.push(...transactionActivities)
|
||||
})
|
||||
|
||||
const sortedActivities = activities.sort((a, b) => a.timestamp - b.timestamp)
|
||||
return filterSortedActivities(sortedActivities)
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
@import './transaction-breakdown-row/index';
|
||||
|
||||
.transaction-breakdown {
|
||||
&__card {
|
||||
background: $white;
|
||||
height: 100%;
|
||||
&__title {
|
||||
border-bottom: 1px solid #d8d8d8;
|
||||
padding-bottom: 4px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
&__row-title {
|
||||
|
@ -2,8 +2,6 @@ import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import TransactionBreakdown from '../transaction-breakdown.component'
|
||||
import TransactionBreakdownRow from '../transaction-breakdown-row'
|
||||
import Card from '../../card'
|
||||
|
||||
describe('TransactionBreakdown Component', () => {
|
||||
it('should render properly', () => {
|
||||
@ -31,7 +29,5 @@ describe('TransactionBreakdown Component', () => {
|
||||
|
||||
assert.ok(wrapper.hasClass('transaction-breakdown'))
|
||||
assert.ok(wrapper.hasClass('test-class'))
|
||||
assert.equal(wrapper.find(Card).length, 1)
|
||||
assert.equal(wrapper.find(Card).find(TransactionBreakdownRow).length, 4)
|
||||
})
|
||||
})
|
||||
|
@ -2,7 +2,6 @@ import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import TransactionBreakdownRow from './transaction-breakdown-row'
|
||||
import Card from '../card'
|
||||
import CurrencyDisplay from '../currency-display'
|
||||
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
|
||||
import HexToDecimal from '../hex-to-decimal'
|
||||
@ -37,10 +36,9 @@ export default class TransactionBreakdown extends PureComponent {
|
||||
|
||||
return (
|
||||
<div className={classnames('transaction-breakdown', className)}>
|
||||
<Card
|
||||
title={t('transaction')}
|
||||
className="transaction-breakdown__card"
|
||||
>
|
||||
<div className="transaction-breakdown__title">
|
||||
{ t('transaction') }
|
||||
</div>
|
||||
<TransactionBreakdownRow title={t('amount')}>
|
||||
<UserPreferencedCurrencyDisplay
|
||||
className="transaction-breakdown__value"
|
||||
@ -93,7 +91,6 @@ export default class TransactionBreakdown extends PureComponent {
|
||||
/>
|
||||
</div>
|
||||
</TransactionBreakdownRow>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
.transaction-list-item-details {
|
||||
&__header {
|
||||
margin-bottom: 8px;
|
||||
margin: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__body {
|
||||
background: #fafbfc;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
&__header-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -45,5 +50,9 @@
|
||||
&__transaction-activity-log {
|
||||
flex: 2;
|
||||
min-width: 0;
|
||||
|
||||
@media screen and (min-width: $break-large) {
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,9 +23,15 @@ describe('TransactionListItemDetails Component', () => {
|
||||
},
|
||||
}
|
||||
|
||||
const transactionGroup = {
|
||||
transactions: [transaction],
|
||||
primaryTransaction: transaction,
|
||||
initialTransaction: transaction,
|
||||
}
|
||||
|
||||
const wrapper = shallow(
|
||||
<TransactionListItemDetails
|
||||
transaction={transaction}
|
||||
transactionGroup={transactionGroup}
|
||||
/>,
|
||||
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
|
||||
)
|
||||
@ -52,9 +58,18 @@ describe('TransactionListItemDetails Component', () => {
|
||||
},
|
||||
}
|
||||
|
||||
const transactionGroup = {
|
||||
transactions: [transaction],
|
||||
primaryTransaction: transaction,
|
||||
initialTransaction: transaction,
|
||||
nonce: '0xa4',
|
||||
hasRetried: false,
|
||||
hasCancelled: false,
|
||||
}
|
||||
|
||||
const wrapper = shallow(
|
||||
<TransactionListItemDetails
|
||||
transaction={transaction}
|
||||
transactionGroup={transactionGroup}
|
||||
showRetry={true}
|
||||
/>,
|
||||
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import SenderToRecipient from '../sender-to-recipient'
|
||||
import { CARDS_VARIANT } from '../sender-to-recipient/sender-to-recipient.constants'
|
||||
import { FLAT_VARIANT } from '../sender-to-recipient/sender-to-recipient.constants'
|
||||
import TransactionActivityLog from '../transaction-activity-log'
|
||||
import TransactionBreakdown from '../transaction-breakdown'
|
||||
import Button from '../button'
|
||||
@ -18,42 +18,43 @@ export default class TransactionListItemDetails extends PureComponent {
|
||||
onRetry: PropTypes.func,
|
||||
showCancel: PropTypes.bool,
|
||||
showRetry: PropTypes.bool,
|
||||
transaction: PropTypes.object,
|
||||
transactionGroup: PropTypes.object,
|
||||
}
|
||||
|
||||
handleEtherscanClick = () => {
|
||||
const { hash, metamaskNetworkId } = this.props.transaction
|
||||
const { transactionGroup: { primaryTransaction } } = this.props
|
||||
const { hash, metamaskNetworkId } = primaryTransaction
|
||||
|
||||
const prefix = prefixForNetwork(metamaskNetworkId)
|
||||
const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
|
||||
|
||||
global.platform.openWindow({ url: etherscanUrl })
|
||||
this.setState({ showTransactionDetails: true })
|
||||
}
|
||||
|
||||
handleCancel = event => {
|
||||
const { onCancel } = this.props
|
||||
const { transactionGroup: { initialTransaction: { id } = {} } = {}, onCancel } = this.props
|
||||
|
||||
event.stopPropagation()
|
||||
onCancel()
|
||||
onCancel(id)
|
||||
}
|
||||
|
||||
handleRetry = event => {
|
||||
const { onRetry } = this.props
|
||||
const { transactionGroup: { initialTransaction: { id } = {} } = {}, onRetry } = this.props
|
||||
|
||||
event.stopPropagation()
|
||||
onRetry()
|
||||
onRetry(id)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
const { transaction, showCancel, showRetry } = this.props
|
||||
const { transactionGroup, showCancel, showRetry, onCancel, onRetry } = this.props
|
||||
const { primaryTransaction: transaction } = transactionGroup
|
||||
const { txParams: { to, from } = {} } = transaction
|
||||
|
||||
return (
|
||||
<div className="transaction-list-item-details">
|
||||
<div className="transaction-list-item-details__header">
|
||||
<div>Details</div>
|
||||
<div>{ t('details') }</div>
|
||||
<div className="transaction-list-item-details__header-buttons">
|
||||
{
|
||||
showRetry && (
|
||||
@ -88,9 +89,10 @@ export default class TransactionListItemDetails extends PureComponent {
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="transaction-list-item-details__body">
|
||||
<div className="transaction-list-item-details__sender-to-recipient-container">
|
||||
<SenderToRecipient
|
||||
variant={CARDS_VARIANT}
|
||||
variant={FLAT_VARIANT}
|
||||
addressOnly
|
||||
recipientAddress={to}
|
||||
senderAddress={from}
|
||||
@ -102,11 +104,14 @@ export default class TransactionListItemDetails extends PureComponent {
|
||||
className="transaction-list-item-details__transaction-breakdown"
|
||||
/>
|
||||
<TransactionActivityLog
|
||||
transaction={transaction}
|
||||
transactionGroup={transactionGroup}
|
||||
className="transaction-list-item-details__transaction-activity-log"
|
||||
onCancel={onCancel}
|
||||
onRetry={onRetry}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -117,12 +117,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__details-container {
|
||||
padding: 8px 16px 16px;
|
||||
background: #f3f4f7;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__expander {
|
||||
max-height: 0px;
|
||||
width: 100%;
|
||||
|
@ -18,6 +18,7 @@ export default class TransactionListItem extends PureComponent {
|
||||
history: PropTypes.object,
|
||||
methodData: PropTypes.object,
|
||||
nonceAndDate: PropTypes.string,
|
||||
primaryTransaction: PropTypes.object,
|
||||
retryTransaction: PropTypes.func,
|
||||
setSelectedToken: PropTypes.func,
|
||||
showCancelModal: PropTypes.func,
|
||||
@ -26,6 +27,7 @@ export default class TransactionListItem extends PureComponent {
|
||||
token: PropTypes.object,
|
||||
tokenData: PropTypes.object,
|
||||
transaction: PropTypes.object,
|
||||
transactionGroup: PropTypes.object,
|
||||
value: PropTypes.string,
|
||||
fetchBasicGasAndTimeEstimates: PropTypes.func,
|
||||
fetchGasEstimates: PropTypes.func,
|
||||
@ -51,36 +53,48 @@ export default class TransactionListItem extends PureComponent {
|
||||
this.setState({ showTransactionDetails: !showTransactionDetails })
|
||||
}
|
||||
|
||||
handleCancel = () => {
|
||||
const { transaction: { id, txParams: { gasPrice } } = {}, showCancelModal } = this.props
|
||||
showCancelModal(id, gasPrice)
|
||||
handleCancel = id => {
|
||||
const {
|
||||
primaryTransaction: { txParams: { gasPrice } } = {},
|
||||
transaction: { id: initialTransactionId },
|
||||
showCancelModal,
|
||||
} = this.props
|
||||
|
||||
const cancelId = id || initialTransactionId
|
||||
showCancelModal(cancelId, gasPrice)
|
||||
}
|
||||
|
||||
handleRetry = () => {
|
||||
/**
|
||||
* @name handleRetry
|
||||
* @description Resubmits a transaction. Retrying a transaction within a list of transactions with
|
||||
* the same nonce requires keeping the original value while increasing the gas price of the latest
|
||||
* transaction.
|
||||
* @param {number} id - Transaction id
|
||||
*/
|
||||
handleRetry = id => {
|
||||
const {
|
||||
transaction: { txParams: { to } = {} },
|
||||
primaryTransaction: { txParams: { gasPrice } } = {},
|
||||
transaction: { txParams: { to } = {}, id: initialTransactionId },
|
||||
methodData: { name } = {},
|
||||
setSelectedToken,
|
||||
retryTransaction,
|
||||
fetchBasicGasAndTimeEstimates,
|
||||
fetchGasEstimates,
|
||||
} = this.props
|
||||
|
||||
if (name === TOKEN_METHOD_TRANSFER) {
|
||||
setSelectedToken(to)
|
||||
}
|
||||
|
||||
return this.resubmit()
|
||||
}
|
||||
const retryId = id || initialTransactionId
|
||||
|
||||
resubmit () {
|
||||
const { transaction, retryTransaction, fetchBasicGasAndTimeEstimates, fetchGasEstimates } = this.props
|
||||
fetchBasicGasAndTimeEstimates().then(basicEstimates => {
|
||||
fetchGasEstimates(basicEstimates.blockTime)
|
||||
}).then(() => {
|
||||
retryTransaction(transaction)
|
||||
})
|
||||
return fetchBasicGasAndTimeEstimates()
|
||||
.then(basicEstimates => fetchGasEstimates(basicEstimates.blockTime))
|
||||
.then(retryTransaction(retryId, gasPrice))
|
||||
}
|
||||
|
||||
renderPrimaryCurrency () {
|
||||
const { token, transaction: { txParams: { data } = {} } = {}, value } = this.props
|
||||
const { token, primaryTransaction: { txParams: { data } = {} } = {}, value } = this.props
|
||||
|
||||
return token
|
||||
? (
|
||||
@ -118,12 +132,14 @@ export default class TransactionListItem extends PureComponent {
|
||||
render () {
|
||||
const {
|
||||
assetImages,
|
||||
transaction,
|
||||
methodData,
|
||||
nonceAndDate,
|
||||
primaryTransaction,
|
||||
showCancel,
|
||||
showRetry,
|
||||
tokenData,
|
||||
transaction,
|
||||
transactionGroup,
|
||||
} = this.props
|
||||
const { txParams = {} } = transaction
|
||||
const { showTransactionDetails } = this.state
|
||||
@ -156,11 +172,11 @@ export default class TransactionListItem extends PureComponent {
|
||||
</div>
|
||||
<TransactionStatus
|
||||
className="transaction-list-item__status"
|
||||
statusKey={getStatusKey(transaction)}
|
||||
statusKey={getStatusKey(primaryTransaction)}
|
||||
title={(
|
||||
(transaction.err && transaction.err.rpc)
|
||||
? transaction.err.rpc.message
|
||||
: transaction.err && transaction.err.message
|
||||
(primaryTransaction.err && primaryTransaction.err.rpc)
|
||||
? primaryTransaction.err.rpc.message
|
||||
: primaryTransaction.err && primaryTransaction.err.message
|
||||
)}
|
||||
/>
|
||||
{ this.renderPrimaryCurrency() }
|
||||
@ -173,7 +189,7 @@ export default class TransactionListItem extends PureComponent {
|
||||
showTransactionDetails && (
|
||||
<div className="transaction-list-item__details-container">
|
||||
<TransactionListItemDetails
|
||||
transaction={transaction}
|
||||
transactionGroup={transactionGroup}
|
||||
onRetry={this.handleRetry}
|
||||
showRetry={showRetry && methodData.done}
|
||||
onCancel={this.handleCancel}
|
||||
|
@ -6,6 +6,7 @@ import TransactionListItem from './transaction-list-item.component'
|
||||
import { setSelectedToken, showModal, showSidebar } from '../../actions'
|
||||
import { hexToDecimal } from '../../helpers/conversions.util'
|
||||
import { getTokenData } from '../../helpers/transactions.util'
|
||||
import { increaseLastGasPrice } from '../../helpers/confirm-transaction/util'
|
||||
import { formatDate } from '../../util'
|
||||
import {
|
||||
fetchBasicGasAndTimeEstimates,
|
||||
@ -14,26 +15,13 @@ import {
|
||||
setCustomGasLimit,
|
||||
} from '../../ducks/gas.duck'
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { transaction: { txParams: { value, nonce, data } = {}, time } = {} } = ownProps
|
||||
|
||||
const tokenData = data && getTokenData(data)
|
||||
const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time)
|
||||
|
||||
return {
|
||||
value,
|
||||
nonceAndDate,
|
||||
tokenData,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()),
|
||||
fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)),
|
||||
setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)),
|
||||
retryTransaction: (transaction) => {
|
||||
dispatch(setCustomGasPrice(transaction.txParams.gasPrice))
|
||||
retryTransaction: (transaction, gasPrice) => {
|
||||
dispatch(setCustomGasPrice(gasPrice || transaction.txParams.gasPrice))
|
||||
dispatch(setCustomGasLimit(transaction.txParams.gas))
|
||||
dispatch(showSidebar({
|
||||
transitionName: 'sidebar-left',
|
||||
@ -47,8 +35,35 @@ const mapDispatchToProps = dispatch => {
|
||||
}
|
||||
}
|
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
const { transactionGroup: { primaryTransaction, initialTransaction } = {} } = ownProps
|
||||
const { retryTransaction, ...restDispatchProps } = dispatchProps
|
||||
const { txParams: { nonce, data } = {}, time } = initialTransaction
|
||||
const { txParams: { value } = {} } = primaryTransaction
|
||||
|
||||
const tokenData = data && getTokenData(data)
|
||||
const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time)
|
||||
|
||||
return {
|
||||
...stateProps,
|
||||
...restDispatchProps,
|
||||
...ownProps,
|
||||
value,
|
||||
nonceAndDate,
|
||||
tokenData,
|
||||
transaction: initialTransaction,
|
||||
primaryTransaction,
|
||||
retryTransaction: (transactionId, gasPrice) => {
|
||||
const { transactionGroup: { transactions = [] } } = ownProps
|
||||
const transaction = transactions.find(tx => tx.id === transactionId) || {}
|
||||
const increasedGasPrice = increaseLastGasPrice(gasPrice)
|
||||
retryTransaction(transaction, increasedGasPrice)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(null, mapDispatchToProps, mergeProps),
|
||||
withMethodData,
|
||||
)(TransactionListItem)
|
||||
|
@ -12,13 +12,11 @@ export default class TransactionList extends PureComponent {
|
||||
static defaultProps = {
|
||||
pendingTransactions: [],
|
||||
completedTransactions: [],
|
||||
transactionToRetry: {},
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
pendingTransactions: PropTypes.array,
|
||||
completedTransactions: PropTypes.array,
|
||||
transactionToRetry: PropTypes.object,
|
||||
selectedToken: PropTypes.object,
|
||||
updateNetworkNonce: PropTypes.func,
|
||||
assetImages: PropTypes.object,
|
||||
@ -37,26 +35,34 @@ export default class TransactionList extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
shouldShowRetry = transaction => {
|
||||
const { transactionToRetry } = this.props
|
||||
const { id, submittedTime } = transaction
|
||||
return id === transactionToRetry.id && Date.now() - submittedTime > 30000
|
||||
shouldShowRetry = (transactionGroup, isEarliestNonce) => {
|
||||
const { transactions = [], hasRetried } = transactionGroup
|
||||
const [earliestTransaction = {}] = transactions
|
||||
const { submittedTime } = earliestTransaction
|
||||
return Date.now() - submittedTime > 30000 && isEarliestNonce && !hasRetried
|
||||
}
|
||||
|
||||
shouldShowCancel (transactionGroup) {
|
||||
const { hasCancelled } = transactionGroup
|
||||
return !hasCancelled
|
||||
}
|
||||
|
||||
renderTransactions () {
|
||||
const { t } = this.context
|
||||
const { pendingTransactions = [], completedTransactions = [] } = this.props
|
||||
const pendingLength = pendingTransactions.length
|
||||
|
||||
return (
|
||||
<div className="transaction-list__transactions">
|
||||
{
|
||||
pendingTransactions.length > 0 && (
|
||||
pendingLength > 0 && (
|
||||
<div className="transaction-list__pending-transactions">
|
||||
<div className="transaction-list__header">
|
||||
{ `${t('queue')} (${pendingTransactions.length})` }
|
||||
</div>
|
||||
{
|
||||
pendingTransactions.map((transaction, index) => (
|
||||
this.renderTransaction(transaction, index, true)
|
||||
pendingTransactions.map((transactionGroup, index) => (
|
||||
this.renderTransaction(transactionGroup, index, true, index === pendingLength - 1)
|
||||
))
|
||||
}
|
||||
</div>
|
||||
@ -68,8 +74,8 @@ export default class TransactionList extends PureComponent {
|
||||
</div>
|
||||
{
|
||||
completedTransactions.length > 0
|
||||
? completedTransactions.map((transaction, index) => (
|
||||
this.renderTransaction(transaction, index)
|
||||
? completedTransactions.map((transactionGroup, index) => (
|
||||
this.renderTransaction(transactionGroup, index)
|
||||
))
|
||||
: this.renderEmpty()
|
||||
}
|
||||
@ -78,21 +84,22 @@ export default class TransactionList extends PureComponent {
|
||||
)
|
||||
}
|
||||
|
||||
renderTransaction (transaction, index, showCancel) {
|
||||
renderTransaction (transactionGroup, index, isPendingTx = false, isEarliestNonce = false) {
|
||||
const { selectedToken, assetImages } = this.props
|
||||
const { transactions = [] } = transactionGroup
|
||||
|
||||
return transaction.key === TRANSACTION_TYPE_SHAPESHIFT
|
||||
return transactions[0].key === TRANSACTION_TYPE_SHAPESHIFT
|
||||
? (
|
||||
<ShapeShiftTransactionListItem
|
||||
{ ...transaction }
|
||||
{ ...transactions[0] }
|
||||
key={`shapeshift${index}`}
|
||||
/>
|
||||
) : (
|
||||
<TransactionListItem
|
||||
transaction={transaction}
|
||||
key={transaction.id}
|
||||
showRetry={this.shouldShowRetry(transaction)}
|
||||
showCancel={showCancel}
|
||||
transactionGroup={transactionGroup}
|
||||
key={`${transactionGroup.nonce}:${index}`}
|
||||
showRetry={isPendingTx && this.shouldShowRetry(transactionGroup, isEarliestNonce)}
|
||||
showCancel={isPendingTx && this.shouldShowCancel(transactionGroup)}
|
||||
token={selectedToken}
|
||||
assetImages={assetImages}
|
||||
/>
|
||||
|
@ -3,24 +3,17 @@ import { withRouter } from 'react-router-dom'
|
||||
import { compose } from 'recompose'
|
||||
import TransactionList from './transaction-list.component'
|
||||
import {
|
||||
pendingTransactionsSelector,
|
||||
submittedPendingTransactionsSelector,
|
||||
completedTransactionsSelector,
|
||||
nonceSortedCompletedTransactionsSelector,
|
||||
nonceSortedPendingTransactionsSelector,
|
||||
} from '../../selectors/transactions'
|
||||
import { getSelectedAddress, getAssetImages } from '../../selectors'
|
||||
import { selectedTokenSelector } from '../../selectors/tokens'
|
||||
import { getLatestSubmittedTxWithNonce } from '../../helpers/transactions.util'
|
||||
import { updateNetworkNonce } from '../../actions'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const pendingTransactions = pendingTransactionsSelector(state)
|
||||
const submittedPendingTransactions = submittedPendingTransactionsSelector(state)
|
||||
const networkNonce = state.appState.networkNonce
|
||||
|
||||
return {
|
||||
completedTransactions: completedTransactionsSelector(state),
|
||||
pendingTransactions,
|
||||
transactionToRetry: getLatestSubmittedTxWithNonce(submittedPendingTransactions, networkNonce),
|
||||
completedTransactions: nonceSortedCompletedTransactionsSelector(state),
|
||||
pendingTransactions: nonceSortedPendingTransactionsSelector(state),
|
||||
selectedToken: selectedTokenSelector(state),
|
||||
selectedAddress: getSelectedAddress(state),
|
||||
assetImages: getAssetImages(state),
|
||||
|
@ -1,6 +1,6 @@
|
||||
.transaction-status {
|
||||
height: 26px;
|
||||
width: 81px;
|
||||
width: 84px;
|
||||
border-radius: 4px;
|
||||
background-color: #f0f0f0;
|
||||
color: #5e6064;
|
||||
@ -12,22 +12,34 @@
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
height: 16px;
|
||||
width: 70px;
|
||||
width: 72px;
|
||||
font-size: .5rem;
|
||||
}
|
||||
|
||||
&--confirmed {
|
||||
background-color: #eafad7;
|
||||
color: #609a1c;
|
||||
|
||||
.transaction-status__transaction-count {
|
||||
border: 1px solid #609a1c;
|
||||
}
|
||||
}
|
||||
|
||||
&--approved, &--submitted {
|
||||
background-color: #FFF2DB;
|
||||
color: #CA810A;
|
||||
|
||||
.transaction-status__transaction-count {
|
||||
border: 1px solid #CA810A;
|
||||
}
|
||||
}
|
||||
|
||||
&--failed {
|
||||
background: lighten($monzo, 56%);
|
||||
color: $monzo;
|
||||
|
||||
.transaction-status__transaction-count {
|
||||
border: 1px solid $monzo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,9 +15,8 @@ describe('TransactionStatus Component', () => {
|
||||
)
|
||||
|
||||
assert.ok(wrapper)
|
||||
const tooltipProps = wrapper.find(Tooltip).props()
|
||||
assert.equal(tooltipProps.children, 'APPROVED')
|
||||
assert.equal(tooltipProps.title, 'test-title')
|
||||
assert.equal(wrapper.text(), 'APPROVED')
|
||||
assert.equal(wrapper.find(Tooltip).props().title, 'test-title')
|
||||
})
|
||||
|
||||
it('should render SUBMITTED properly', () => {
|
||||
@ -29,7 +28,6 @@ describe('TransactionStatus Component', () => {
|
||||
)
|
||||
|
||||
assert.ok(wrapper)
|
||||
const tooltipProps = wrapper.find(Tooltip).props()
|
||||
assert.equal(tooltipProps.children, 'PENDING')
|
||||
assert.equal(wrapper.text(), 'PENDING')
|
||||
})
|
||||
})
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
CONFIRMED_STATUS,
|
||||
FAILED_STATUS,
|
||||
DROPPED_STATUS,
|
||||
CANCELLED_STATUS,
|
||||
} from '../../constants/transactions'
|
||||
|
||||
const statusToClassNameHash = {
|
||||
@ -22,6 +23,7 @@ const statusToClassNameHash = {
|
||||
[CONFIRMED_STATUS]: 'transaction-status--confirmed',
|
||||
[FAILED_STATUS]: 'transaction-status--failed',
|
||||
[DROPPED_STATUS]: 'transaction-status--dropped',
|
||||
[CANCELLED_STATUS]: 'transaction-status--failed',
|
||||
}
|
||||
|
||||
const statusToTextHash = {
|
||||
@ -49,7 +51,10 @@ export default class TransactionStatus extends PureComponent {
|
||||
|
||||
return (
|
||||
<div className={classnames('transaction-status', className, statusToClassNameHash[statusKey])}>
|
||||
<Tooltip position="top" title={title}>
|
||||
<Tooltip
|
||||
position="top"
|
||||
title={title}
|
||||
>
|
||||
{ statusText }
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
@ -6,6 +6,7 @@ export const SUBMITTED_STATUS = 'submitted'
|
||||
export const CONFIRMED_STATUS = 'confirmed'
|
||||
export const FAILED_STATUS = 'failed'
|
||||
export const DROPPED_STATUS = 'dropped'
|
||||
export const CANCELLED_STATUS = 'cancelled'
|
||||
|
||||
export const TOKEN_METHOD_TRANSFER = 'transfer'
|
||||
export const TOKEN_METHOD_APPROVE = 'approve'
|
||||
@ -17,7 +18,7 @@ export const APPROVE_ACTION_KEY = 'approve'
|
||||
export const SEND_TOKEN_ACTION_KEY = 'sentTokens'
|
||||
export const TRANSFER_FROM_ACTION_KEY = 'transferFrom'
|
||||
export const SIGNATURE_REQUEST_KEY = 'signatureRequest'
|
||||
export const UNKNOWN_FUNCTION_KEY = 'unknownFunction'
|
||||
export const CONTRACT_INTERACTION_KEY = 'contractInteraction'
|
||||
export const CANCEL_ATTEMPT_ACTION_KEY = 'cancelAttempt'
|
||||
|
||||
export const TRANSACTION_TYPE_SHAPESHIFT = 'shapeshift'
|
||||
|
@ -4,6 +4,9 @@ import {
|
||||
loadLocalStorageData,
|
||||
saveLocalStorageData,
|
||||
} from '../../lib/local-storage-helpers'
|
||||
import {
|
||||
decGWEIToHexWEI,
|
||||
} from '../helpers/conversions.util'
|
||||
|
||||
// Actions
|
||||
const BASIC_GAS_ESTIMATE_LOADING_FINISHED = 'metamask/gas/BASIC_GAS_ESTIMATE_LOADING_FINISHED'
|
||||
@ -403,6 +406,17 @@ export function fetchGasEstimates (blockTime) {
|
||||
}
|
||||
}
|
||||
|
||||
export function setCustomGasPriceForRetry (newPrice) {
|
||||
return (dispatch) => {
|
||||
if (newPrice !== '0x0') {
|
||||
dispatch(setCustomGasPrice(newPrice))
|
||||
} else {
|
||||
const { fast } = loadLocalStorageData('BASIC_PRICE_ESTIMATES')
|
||||
dispatch(setCustomGasPrice(decGWEIToHexWEI(fast)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setBasicGasEstimateData (basicGasEstimateData) {
|
||||
return {
|
||||
type: SET_BASIC_GAS_ESTIMATE_DATA,
|
||||
|
@ -2,6 +2,10 @@ import ethUtil from 'ethereumjs-util'
|
||||
import MethodRegistry from 'eth-method-registry'
|
||||
import abi from 'human-standard-token-abi'
|
||||
import abiDecoder from 'abi-decoder'
|
||||
import {
|
||||
TRANSACTION_TYPE_CANCEL,
|
||||
TRANSACTION_STATUS_CONFIRMED,
|
||||
} from '../../../app/scripts/controllers/transactions/enums'
|
||||
|
||||
import {
|
||||
TOKEN_METHOD_TRANSFER,
|
||||
@ -13,7 +17,7 @@ import {
|
||||
SEND_TOKEN_ACTION_KEY,
|
||||
TRANSFER_FROM_ACTION_KEY,
|
||||
SIGNATURE_REQUEST_KEY,
|
||||
UNKNOWN_FUNCTION_KEY,
|
||||
CONTRACT_INTERACTION_KEY,
|
||||
CANCEL_ATTEMPT_ACTION_KEY,
|
||||
} from '../constants/transactions'
|
||||
|
||||
@ -87,7 +91,7 @@ export async function getTransactionActionKey (transaction, methodData) {
|
||||
const methodName = name && name.toLowerCase()
|
||||
|
||||
if (!methodName) {
|
||||
return UNKNOWN_FUNCTION_KEY
|
||||
return CONTRACT_INTERACTION_KEY
|
||||
}
|
||||
|
||||
switch (methodName) {
|
||||
@ -148,12 +152,16 @@ export function sumHexes (...args) {
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getStatusKey (transaction) {
|
||||
const { txReceipt: { status } = {} } = transaction
|
||||
const { txReceipt: { status: receiptStatus } = {}, type, status } = transaction
|
||||
|
||||
// There was an on-chain failure
|
||||
if (status === '0x0') {
|
||||
if (receiptStatus === '0x0') {
|
||||
return 'failed'
|
||||
}
|
||||
|
||||
if (status === TRANSACTION_STATUS_CONFIRMED && type === TRANSACTION_TYPE_CANCEL) {
|
||||
return 'cancelled'
|
||||
}
|
||||
|
||||
return transaction.status
|
||||
}
|
||||
|
@ -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,10 +68,193 @@ 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()
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* @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(({ primaryTransaction }) => {
|
||||
return !(primaryTransaction.status in pendingStatusHash)
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
@ -49,10 +264,3 @@ export const submittedPendingTransactionsSelector = createSelector(
|
||||
transactions.filter(transaction => transaction.status === SUBMITTED_STATUS)
|
||||
)
|
||||
)
|
||||
|
||||
export const completedTransactionsSelector = createSelector(
|
||||
transactionsSelector,
|
||||
(transactions = []) => (
|
||||
transactions.filter(transaction => !(transaction.status in pendingStatusHash))
|
||||
)
|
||||
)
|
||||
|
@ -8,8 +8,8 @@ const GWEI_FACTOR = new ethUtil.BN(1e9)
|
||||
const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR)
|
||||
|
||||
// formatData :: ( date: <Unix Timestamp> ) -> String
|
||||
function formatDate (date) {
|
||||
return vreme.format(new Date(date), '3/16/2014 at 14:30')
|
||||
function formatDate (date, format = '3/16/2014 at 14:30') {
|
||||
return vreme.format(new Date(date), format)
|
||||
}
|
||||
|
||||
var valueTable = {
|
||||
|
@ -21,7 +21,7 @@ module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, typedMes
|
||||
allValues = allValues.concat(typedValues)
|
||||
|
||||
allValues = allValues.sort((a, b) => {
|
||||
return a.time > b.time
|
||||
return a.time - b.time
|
||||
})
|
||||
|
||||
return allValues
|
||||
|
Loading…
Reference in New Issue
Block a user