Merge pull request #5182 from MetaMask/tx-activity
Add Transaction Details to the Transaction List viewfeature/default_network_editable
commit
16d6cd5eb9
After Width: | Height: | Size: 795 B |
@ -0,0 +1,25 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import classnames from 'classnames' |
||||
|
||||
export default class Card extends PureComponent { |
||||
static propTypes = { |
||||
className: PropTypes.string, |
||||
overrideClassName: PropTypes.bool, |
||||
title: PropTypes.string, |
||||
children: PropTypes.node, |
||||
} |
||||
|
||||
render () { |
||||
const { className, overrideClassName, title } = this.props |
||||
|
||||
return ( |
||||
<div className={classnames({ 'card': !overrideClassName }, className)}> |
||||
<div className="card__title"> |
||||
{ title } |
||||
</div> |
||||
{ this.props.children } |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './card.component' |
@ -0,0 +1,11 @@ |
||||
.card { |
||||
border-radius: 4px; |
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08); |
||||
padding: 8px; |
||||
|
||||
&__title { |
||||
border-bottom: 1px solid #d8d8d8; |
||||
padding-bottom: 4px; |
||||
text-transform: capitalize; |
||||
} |
||||
} |
@ -0,0 +1,25 @@ |
||||
import React from 'react' |
||||
import assert from 'assert' |
||||
import { shallow } from 'enzyme' |
||||
import Card from '../card.component' |
||||
|
||||
describe('Card Component', () => { |
||||
it('should render a card with a title and child element', () => { |
||||
const wrapper = shallow( |
||||
<Card |
||||
title="Test" |
||||
className="card-test-class" |
||||
> |
||||
<div className="child-test-class">Child</div> |
||||
</Card> |
||||
) |
||||
|
||||
assert.ok(wrapper.hasClass('card-test-class')) |
||||
const title = wrapper.find('.card__title') |
||||
assert.ok(title) |
||||
assert.equal(title.text(), 'Test') |
||||
const child = wrapper.find('.child-test-class') |
||||
assert.ok(child) |
||||
assert.equal(child.text(), 'Child') |
||||
}) |
||||
}) |
@ -1,161 +0,0 @@ |
||||
const Component = require('react').Component |
||||
const PropTypes = require('prop-types') |
||||
const h = require('react-hyperscript') |
||||
const inherits = require('util').inherits |
||||
const ethUtil = require('ethereumjs-util') |
||||
const BN = ethUtil.BN |
||||
const extend = require('xtend') |
||||
const connect = require('react-redux').connect |
||||
|
||||
HexAsDecimalInput.contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
module.exports = connect()(HexAsDecimalInput) |
||||
|
||||
|
||||
inherits(HexAsDecimalInput, Component) |
||||
function HexAsDecimalInput () { |
||||
this.state = { invalid: null } |
||||
Component.call(this) |
||||
} |
||||
|
||||
/* Hex as Decimal Input |
||||
* |
||||
* A component for allowing easy, decimal editing |
||||
* of a passed in hex string value. |
||||
* |
||||
* On change, calls back its `onChange` function parameter |
||||
* and passes it an updated hex string. |
||||
*/ |
||||
|
||||
HexAsDecimalInput.prototype.render = function () { |
||||
const props = this.props |
||||
const state = this.state |
||||
|
||||
const { value, onChange, min, max } = props |
||||
|
||||
const toEth = props.toEth |
||||
const suffix = props.suffix |
||||
const decimalValue = decimalize(value, toEth) |
||||
const style = props.style |
||||
|
||||
return ( |
||||
h('.flex-column', [ |
||||
h('.flex-row', { |
||||
style: { |
||||
alignItems: 'flex-end', |
||||
lineHeight: '13px', |
||||
fontFamily: 'Montserrat Light', |
||||
textRendering: 'geometricPrecision', |
||||
}, |
||||
}, [ |
||||
h('input.hex-input', { |
||||
type: 'number', |
||||
required: true, |
||||
min: min, |
||||
max: max, |
||||
style: extend({ |
||||
display: 'block', |
||||
textAlign: 'right', |
||||
backgroundColor: 'transparent', |
||||
border: '1px solid #bdbdbd', |
||||
|
||||
}, style), |
||||
value: parseInt(decimalValue), |
||||
onBlur: (event) => { |
||||
this.updateValidity(event) |
||||
}, |
||||
onChange: (event) => { |
||||
this.updateValidity(event) |
||||
const hexString = (event.target.value === '') ? '' : hexify(event.target.value) |
||||
onChange(hexString) |
||||
}, |
||||
onInvalid: (event) => { |
||||
const msg = this.constructWarning() |
||||
if (msg === state.invalid) { |
||||
return |
||||
} |
||||
this.setState({ invalid: msg }) |
||||
event.preventDefault() |
||||
return false |
||||
}, |
||||
}), |
||||
h('div', { |
||||
style: { |
||||
color: ' #AEAEAE', |
||||
fontSize: '12px', |
||||
marginLeft: '5px', |
||||
marginRight: '6px', |
||||
width: '20px', |
||||
}, |
||||
}, suffix), |
||||
]), |
||||
|
||||
state.invalid ? h('span.error', { |
||||
style: { |
||||
position: 'absolute', |
||||
right: '0px', |
||||
textAlign: 'right', |
||||
transform: 'translateY(26px)', |
||||
padding: '3px', |
||||
background: 'rgba(255,255,255,0.85)', |
||||
zIndex: '1', |
||||
textTransform: 'capitalize', |
||||
border: '2px solid #E20202', |
||||
}, |
||||
}, state.invalid) : null, |
||||
]) |
||||
) |
||||
} |
||||
|
||||
HexAsDecimalInput.prototype.setValid = function (message) { |
||||
this.setState({ invalid: null }) |
||||
} |
||||
|
||||
HexAsDecimalInput.prototype.updateValidity = function (event) { |
||||
const target = event.target |
||||
const value = this.props.value |
||||
const newValue = target.value |
||||
|
||||
if (value === newValue) { |
||||
return |
||||
} |
||||
|
||||
const valid = target.checkValidity() |
||||
if (valid) { |
||||
this.setState({ invalid: null }) |
||||
} |
||||
} |
||||
|
||||
HexAsDecimalInput.prototype.constructWarning = function () { |
||||
const { name, min, max } = this.props |
||||
let message = name ? name + ' ' : '' |
||||
|
||||
if (min && max) { |
||||
message += this.context.t('betweenMinAndMax', [min, max]) |
||||
} else if (min) { |
||||
message += this.context.t('greaterThanMin', [min]) |
||||
} else if (max) { |
||||
message += this.context.t('lessThanMax', [max]) |
||||
} else { |
||||
message += this.context.t('invalidInput') |
||||
} |
||||
|
||||
return message |
||||
} |
||||
|
||||
function hexify (decimalString) { |
||||
const hexBN = new BN(parseInt(decimalString), 10) |
||||
return '0x' + hexBN.toString('hex') |
||||
} |
||||
|
||||
function decimalize (input, toEth) { |
||||
if (input === '') { |
||||
return '' |
||||
} else { |
||||
const strippedInput = ethUtil.stripHexPrefix(input) |
||||
const inputBN = new BN(strippedInput, 'hex') |
||||
return inputBN.toString(10) |
||||
} |
||||
} |
@ -0,0 +1,21 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import { hexToDecimal } from '../../helpers/conversions.util' |
||||
|
||||
export default class HexToDecimal extends PureComponent { |
||||
static propTypes = { |
||||
className: PropTypes.string, |
||||
value: PropTypes.string, |
||||
} |
||||
|
||||
render () { |
||||
const { className, value } = this.props |
||||
const decimalValue = hexToDecimal(value) |
||||
|
||||
return ( |
||||
<div className={className}> |
||||
{ decimalValue } |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './hex-to-decimal.component' |
@ -0,0 +1,26 @@ |
||||
import React from 'react' |
||||
import assert from 'assert' |
||||
import { shallow } from 'enzyme' |
||||
import HexToDecimal from '../hex-to-decimal.component' |
||||
|
||||
describe('HexToDecimal Component', () => { |
||||
it('should render a prefixed hex as a decimal with a className', () => { |
||||
const wrapper = shallow(<HexToDecimal |
||||
value="0x3039" |
||||
className="hex-to-decimal" |
||||
/>) |
||||
|
||||
assert.ok(wrapper.hasClass('hex-to-decimal')) |
||||
assert.equal(wrapper.text(), '12345') |
||||
}) |
||||
|
||||
it('should render an unprefixed hex as a decimal with a className', () => { |
||||
const wrapper = shallow(<HexToDecimal |
||||
value="1A85" |
||||
className="hex-to-decimal" |
||||
/>) |
||||
|
||||
assert.ok(wrapper.hasClass('hex-to-decimal')) |
||||
assert.equal(wrapper.text(), '6789') |
||||
}) |
||||
}) |
@ -0,0 +1 @@ |
||||
export { default } from './transaction-activity-log.container' |
@ -0,0 +1,63 @@ |
||||
.transaction-activity-log { |
||||
&__card { |
||||
background: $white; |
||||
height: 100%; |
||||
} |
||||
|
||||
&__activities-container { |
||||
padding-top: 8px; |
||||
} |
||||
|
||||
&__activity { |
||||
padding: 4px 0; |
||||
display: flex; |
||||
flex-direction: row; |
||||
align-items: center; |
||||
position: relative; |
||||
|
||||
&::after { |
||||
content: ''; |
||||
position: absolute; |
||||
left: 0; |
||||
top: 0; |
||||
height: 100%; |
||||
width: 6px; |
||||
border-right: 1px solid $scorpion; |
||||
} |
||||
|
||||
&:first-child::after { |
||||
height: 50%; |
||||
top: 50%; |
||||
} |
||||
|
||||
&:last-child::after { |
||||
height: 50%; |
||||
} |
||||
} |
||||
|
||||
&__activity-icon { |
||||
width: 13px; |
||||
height: 13px; |
||||
margin-right: 6px; |
||||
border-radius: 50%; |
||||
background: $scorpion; |
||||
flex: 0 0 auto; |
||||
} |
||||
|
||||
&__activity-text { |
||||
color: $scorpion; |
||||
font-size: .75rem; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
} |
||||
|
||||
&__value { |
||||
display: inline; |
||||
font-weight: 500; |
||||
} |
||||
|
||||
b { |
||||
font-weight: 500; |
||||
} |
||||
} |
@ -0,0 +1,35 @@ |
||||
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', |
||||
value: '0x2386f26fc10000', |
||||
}, |
||||
} |
||||
|
||||
const wrapper = shallow( |
||||
<TransactionActivityLog |
||||
transaction={transaction} |
||||
className="test-class" |
||||
/>, |
||||
{ 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) |
||||
}) |
||||
}) |
@ -0,0 +1,27 @@ |
||||
import assert from 'assert' |
||||
import proxyquire from 'proxyquire' |
||||
|
||||
let mapStateToProps |
||||
|
||||
proxyquire('../transaction-activity-log.container.js', { |
||||
'react-redux': { |
||||
connect: ms => { |
||||
mapStateToProps = ms |
||||
return () => ({}) |
||||
}, |
||||
}, |
||||
}) |
||||
|
||||
describe('TransactionActivityLog container', () => { |
||||
describe('mapStateToProps()', () => { |
||||
it('should return the correct props', () => { |
||||
const mockState = { |
||||
metamask: { |
||||
conversionRate: 280.45, |
||||
}, |
||||
} |
||||
|
||||
assert.deepEqual(mapStateToProps(mockState), { conversionRate: 280.45 }) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,208 @@ |
||||
import assert from 'assert' |
||||
import { getActivities } from '../transaction-activity-log.util' |
||||
|
||||
describe('getActivities', () => { |
||||
it('should return no activities for an empty history', () => { |
||||
const transaction = { |
||||
history: [], |
||||
id: 1, |
||||
status: 'confirmed', |
||||
txParams: { |
||||
from: '0x1', |
||||
gas: '0x5208', |
||||
gasPrice: '0x3b9aca00', |
||||
nonce: '0xa4', |
||||
to: '0x2', |
||||
value: '0x2386f26fc10000', |
||||
}, |
||||
} |
||||
|
||||
assert.deepEqual(getActivities(transaction), []) |
||||
}) |
||||
|
||||
it('should return activities for a transaction\'s history', () => { |
||||
const transaction = { |
||||
history: [ |
||||
{ |
||||
id: 5559712943815343, |
||||
loadingDefaults: true, |
||||
metamaskNetworkId: '3', |
||||
status: 'unapproved', |
||||
time: 1535507561452, |
||||
txParams: { |
||||
from: '0x1', |
||||
gas: '0x5208', |
||||
gasPrice: '0x3b9aca00', |
||||
nonce: '0xa4', |
||||
to: '0x2', |
||||
value: '0x2386f26fc10000', |
||||
}, |
||||
}, |
||||
[ |
||||
{ |
||||
op: 'replace', |
||||
path: '/loadingDefaults', |
||||
timestamp: 1535507561515, |
||||
value: false, |
||||
}, |
||||
{ |
||||
op: 'add', |
||||
path: '/gasPriceSpecified', |
||||
value: true, |
||||
}, |
||||
{ |
||||
op: 'add', |
||||
path: '/gasLimitSpecified', |
||||
value: true, |
||||
}, |
||||
{ |
||||
op: 'add', |
||||
path: '/estimatedGas', |
||||
value: '0x5208', |
||||
}, |
||||
], |
||||
[ |
||||
{ |
||||
note: '#newUnapprovedTransaction - adding the origin', |
||||
op: 'add', |
||||
path: '/origin', |
||||
timestamp: 1535507561516, |
||||
value: 'MetaMask', |
||||
}, |
||||
[], |
||||
], |
||||
[ |
||||
{ |
||||
note: 'confTx: user approved transaction', |
||||
op: 'replace', |
||||
path: '/txParams/gasPrice', |
||||
timestamp: 1535664571504, |
||||
value: '0x77359400', |
||||
}, |
||||
], |
||||
[ |
||||
{ |
||||
note: 'txStateManager: setting status to approved', |
||||
op: 'replace', |
||||
path: '/status', |
||||
timestamp: 1535507564302, |
||||
value: 'approved', |
||||
}, |
||||
], |
||||
[ |
||||
{ |
||||
note: 'transactions#approveTransaction', |
||||
op: 'add', |
||||
path: '/txParams/nonce', |
||||
timestamp: 1535507564439, |
||||
value: '0xa4', |
||||
}, |
||||
{ |
||||
op: 'add', |
||||
path: '/nonceDetails', |
||||
value: { |
||||
local: {}, |
||||
network: {}, |
||||
params: {}, |
||||
}, |
||||
}, |
||||
], |
||||
[ |
||||
{ |
||||
note: 'transactions#publishTransaction', |
||||
op: 'replace', |
||||
path: '/status', |
||||
timestamp: 1535507564518, |
||||
value: 'signed', |
||||
}, |
||||
{ |
||||
op: 'add', |
||||
path: '/rawTx', |
||||
value: '0xf86b81a4843b9aca008252089450a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706872386f26fc10000802aa007b30119fc4fc5954fad727895b7e3ba80a78d197e95703cc603bcf017879151a01c50beda40ffaee541da9c05b9616247074f25f392800e0ad6c7a835d5366edf', |
||||
}, |
||||
], |
||||
[], |
||||
[ |
||||
{ |
||||
note: 'transactions#setTxHash', |
||||
op: 'add', |
||||
path: '/hash', |
||||
timestamp: 1535507564658, |
||||
value: '0x7acc4987b5c0dfa8d423798a8c561138259de1f98a62e3d52e7e83c0e0dd9fb7', |
||||
}, |
||||
], |
||||
[ |
||||
{ |
||||
note: 'txStateManager - add submitted time stamp', |
||||
op: 'add', |
||||
path: '/submittedTime', |
||||
timestamp: 1535507564660, |
||||
value: 1535507564660, |
||||
}, |
||||
], |
||||
[ |
||||
{ |
||||
note: 'txStateManager: setting status to submitted', |
||||
op: 'replace', |
||||
path: '/status', |
||||
timestamp: 1535507564665, |
||||
value: 'submitted', |
||||
}, |
||||
], |
||||
[ |
||||
{ |
||||
note: 'transactions/pending-tx-tracker#event: tx:block-update', |
||||
op: 'add', |
||||
path: '/firstRetryBlockNumber', |
||||
timestamp: 1535507575476, |
||||
value: '0x3bf624', |
||||
}, |
||||
], |
||||
[ |
||||
{ |
||||
note: 'txStateManager: setting status to confirmed', |
||||
op: 'replace', |
||||
path: '/status', |
||||
timestamp: 1535507615993, |
||||
value: 'confirmed', |
||||
}, |
||||
], |
||||
], |
||||
id: 1, |
||||
status: 'confirmed', |
||||
txParams: { |
||||
from: '0x1', |
||||
gas: '0x5208', |
||||
gasPrice: '0x3b9aca00', |
||||
nonce: '0xa4', |
||||
to: '0x2', |
||||
value: '0x2386f26fc10000', |
||||
}, |
||||
} |
||||
|
||||
const expectedResult = [ |
||||
{ |
||||
'eventKey': 'transactionCreated', |
||||
'timestamp': 1535507561452, |
||||
'value': '0x2386f26fc10000', |
||||
}, |
||||
{ |
||||
'eventKey': 'transactionUpdatedGas', |
||||
'timestamp': 1535664571504, |
||||
'value': '0x77359400', |
||||
}, |
||||
{ |
||||
'eventKey': 'transactionSubmitted', |
||||
'timestamp': 1535507564665, |
||||
'value': undefined, |
||||
}, |
||||
{ |
||||
'eventKey': 'transactionConfirmed', |
||||
'timestamp': 1535507615993, |
||||
'value': undefined, |
||||
}, |
||||
] |
||||
|
||||
assert.deepEqual(getActivities(transaction), expectedResult) |
||||
}) |
||||
}) |
@ -0,0 +1,91 @@ |
||||
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 { ETH } from '../../constants/common' |
||||
import { formatDate } from '../../util' |
||||
|
||||
export default class TransactionActivityLog extends PureComponent { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
transaction: PropTypes.object, |
||||
className: PropTypes.string, |
||||
conversionRate: PropTypes.number, |
||||
} |
||||
|
||||
state = { |
||||
activities: [], |
||||
} |
||||
|
||||
componentDidMount () { |
||||
this.setActivites() |
||||
} |
||||
|
||||
componentDidUpdate (prevProps) { |
||||
const { transaction: { history: prevHistory = [] } = {} } = prevProps |
||||
const { transaction: { history = [] } = {} } = this.props |
||||
|
||||
if (prevHistory.length !== history.length) { |
||||
this.setActivites() |
||||
} |
||||
} |
||||
|
||||
setActivites () { |
||||
const activities = getActivities(this.props.transaction) |
||||
this.setState({ activities }) |
||||
} |
||||
|
||||
renderActivity (activity, index) { |
||||
const { conversionRate } = this.props |
||||
const { eventKey, value, timestamp } = activity |
||||
const ethValue = index === 0 |
||||
? `${getValueFromWeiHex({ |
||||
value, |
||||
toCurrency: ETH, |
||||
conversionRate, |
||||
numberOfDecimals: 6, |
||||
})} ${ETH}` |
||||
: getEthConversionFromWeiHex({ value, toCurrency: ETH, conversionRate }) |
||||
const formattedTimestamp = formatDate(timestamp) |
||||
const activityText = this.context.t(eventKey, [ethValue, formattedTimestamp]) |
||||
|
||||
return ( |
||||
<div |
||||
key={index} |
||||
className="transaction-activity-log__activity" |
||||
> |
||||
<div className="transaction-activity-log__activity-icon" /> |
||||
<div |
||||
className="transaction-activity-log__activity-text" |
||||
title={activityText} |
||||
> |
||||
{ activityText } |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
render () { |
||||
const { t } = this.context |
||||
const { className } = this.props |
||||
const { activities } = this.state |
||||
|
||||
return ( |
||||
<div className={classnames('transaction-activity-log', className)}> |
||||
<Card |
||||
title={t('activityLog')} |
||||
className="transaction-activity-log__card" |
||||
> |
||||
<div className="transaction-activity-log__activities-container"> |
||||
{ activities.map((activity, index) => this.renderActivity(activity, index)) } |
||||
</div> |
||||
</Card> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,11 @@ |
||||
import { connect } from 'react-redux' |
||||
import TransactionActivityLog from './transaction-activity-log.component' |
||||
import { conversionRateSelector } from '../../selectors' |
||||
|
||||
const mapStateToProps = state => { |
||||
return { |
||||
conversionRate: conversionRateSelector(state), |
||||
} |
||||
} |
||||
|
||||
export default connect(mapStateToProps)(TransactionActivityLog) |
@ -0,0 +1,82 @@ |
||||
// 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' |
||||
|
||||
// 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 eventPathsHash = { |
||||
[STATUS_PATH]: true, |
||||
[GAS_PRICE_PATH]: true, |
||||
} |
||||
|
||||
const statusHash = { |
||||
[SUBMITTED_STATUS]: TRANSACTION_SUBMITTED_EVENT, |
||||
[CONFIRMED_STATUS]: TRANSACTION_CONFIRMED_EVENT, |
||||
[DROPPED_STATUS]: TRANSACTION_DROPPED_EVENT, |
||||
} |
||||
|
||||
function eventCreator (eventKey, timestamp, value) { |
||||
return { |
||||
eventKey, |
||||
timestamp, |
||||
value, |
||||
} |
||||
} |
||||
|
||||
export function getActivities (transaction) { |
||||
const { history = [] } = transaction |
||||
|
||||
return 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)) |
||||
} else if (Array.isArray(base)) { |
||||
const events = [] |
||||
|
||||
base.forEach(entry => { |
||||
const { op, path, value, timestamp } = entry |
||||
|
||||
if (path in eventPathsHash && op === REPLACE_OP) { |
||||
switch (path) { |
||||
case STATUS_PATH: { |
||||
if (value in statusHash) { |
||||
events.push(eventCreator(statusHash[value], timestamp)) |
||||
} |
||||
|
||||
break |
||||
} |
||||
|
||||
case GAS_PRICE_PATH: { |
||||
events.push(eventCreator(TRANSACTION_UPDATED_GAS_EVENT, timestamp, value)) |
||||
break |
||||
} |
||||
|
||||
default: { |
||||
events.push(eventCreator(TRANSACTION_UPDATED_EVENT, timestamp)) |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
|
||||
return acc.concat(events) |
||||
} |
||||
|
||||
return acc |
||||
}, []) |
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './transaction-breakdown.component' |
@ -0,0 +1,23 @@ |
||||
@import './transaction-breakdown-row/index'; |
||||
|
||||
.transaction-breakdown { |
||||
&__card { |
||||
background: $white; |
||||
height: 100%; |
||||
} |
||||
|
||||
&__row-title { |
||||
text-transform: capitalize; |
||||
} |
||||
|
||||
&__value { |
||||
text-align: end; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
|
||||
&--eth-total { |
||||
font-weight: 500; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,37 @@ |
||||
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', () => { |
||||
const transaction = { |
||||
history: [], |
||||
id: 1, |
||||
status: 'confirmed', |
||||
txParams: { |
||||
from: '0x1', |
||||
gas: '0x5208', |
||||
gasPrice: '0x3b9aca00', |
||||
nonce: '0xa4', |
||||
to: '0x2', |
||||
value: '0x2386f26fc10000', |
||||
}, |
||||
} |
||||
|
||||
const wrapper = shallow( |
||||
<TransactionBreakdown |
||||
transaction={transaction} |
||||
className="test-class" |
||||
/>, |
||||
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } } |
||||
) |
||||
|
||||
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) |
||||
}) |
||||
}) |
@ -0,0 +1 @@ |
||||
export { default } from './transaction-breakdown-row.component' |
@ -0,0 +1,19 @@ |
||||
.transaction-breakdown-row { |
||||
font-size: .75rem; |
||||
color: $scorpion; |
||||
display: flex; |
||||
justify-content: space-between; |
||||
padding: 8px 0; |
||||
|
||||
&:not(:last-child) { |
||||
border-bottom: 1px solid #d8d8d8; |
||||
} |
||||
|
||||
&__title { |
||||
padding-right: 8px; |
||||
} |
||||
|
||||
&__value { |
||||
min-width: 0; |
||||
} |
||||
} |
@ -0,0 +1,39 @@ |
||||
import React from 'react' |
||||
import assert from 'assert' |
||||
import { shallow } from 'enzyme' |
||||
import TransactionBreakdownRow from '../transaction-breakdown-row.component' |
||||
import Button from '../../../button' |
||||
|
||||
describe('TransactionBreakdownRow Component', () => { |
||||
it('should render text properly', () => { |
||||
const wrapper = shallow( |
||||
<TransactionBreakdownRow |
||||
title="test" |
||||
className="test-class" |
||||
> |
||||
Test |
||||
</TransactionBreakdownRow>, |
||||
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } } |
||||
) |
||||
|
||||
assert.ok(wrapper.hasClass('transaction-breakdown-row')) |
||||
assert.equal(wrapper.find('.transaction-breakdown-row__title').text(), 'test') |
||||
assert.equal(wrapper.find('.transaction-breakdown-row__value').text(), 'Test') |
||||
}) |
||||
|
||||
it('should render components properly', () => { |
||||
const wrapper = shallow( |
||||
<TransactionBreakdownRow |
||||
title="test" |
||||
className="test-class" |
||||
> |
||||
<Button onClick={() => {}} >Button</Button> |
||||
</TransactionBreakdownRow>, |
||||
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } } |
||||
) |
||||
|
||||
assert.ok(wrapper.hasClass('transaction-breakdown-row')) |
||||
assert.equal(wrapper.find('.transaction-breakdown-row__title').text(), 'test') |
||||
assert.ok(wrapper.find('.transaction-breakdown-row__value').find(Button)) |
||||
}) |
||||
}) |
@ -0,0 +1,26 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import classnames from 'classnames' |
||||
|
||||
export default class TransactionBreakdownRow extends PureComponent { |
||||
static propTypes = { |
||||
title: PropTypes.string, |
||||
children: PropTypes.node, |
||||
className: PropTypes.string, |
||||
} |
||||
|
||||
render () { |
||||
const { title, children, className } = this.props |
||||
|
||||
return ( |
||||
<div className={classnames('transaction-breakdown-row', className)}> |
||||
<div className="transaction-breakdown-row__title"> |
||||
{ title } |
||||
</div> |
||||
<div className="transaction-breakdown-row__value"> |
||||
{ children } |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,82 @@ |
||||
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 HexToDecimal from '../hex-to-decimal' |
||||
import { ETH, GWEI } from '../../constants/common' |
||||
import { getHexGasTotal } from '../../helpers/confirm-transaction/util' |
||||
import { sumHexes } from '../../helpers/transactions.util' |
||||
|
||||
export default class TransactionBreakdown extends PureComponent { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
transaction: PropTypes.object, |
||||
className: PropTypes.string, |
||||
} |
||||
|
||||
static defaultProps = { |
||||
transaction: {}, |
||||
} |
||||
|
||||
render () { |
||||
const { t } = this.context |
||||
const { transaction, className } = this.props |
||||
const { txParams: { gas, gasPrice, value } = {} } = transaction |
||||
const hexGasTotal = getHexGasTotal({ gasLimit: gas, gasPrice }) |
||||
const totalInHex = sumHexes(hexGasTotal, value) |
||||
|
||||
return ( |
||||
<div className={classnames('transaction-breakdown', className)}> |
||||
<Card |
||||
title={t('transaction')} |
||||
className="transaction-breakdown__card" |
||||
> |
||||
<TransactionBreakdownRow title={t('amount')}> |
||||
<CurrencyDisplay |
||||
className="transaction-breakdown__value" |
||||
currency={ETH} |
||||
value={value} |
||||
/> |
||||
</TransactionBreakdownRow> |
||||
<TransactionBreakdownRow |
||||
title={`${t('gasLimit')} (${t('units')})`} |
||||
className="transaction-breakdown__row-title" |
||||
> |
||||
<HexToDecimal |
||||
className="transaction-breakdown__value" |
||||
value={gas} |
||||
/> |
||||
</TransactionBreakdownRow> |
||||
<TransactionBreakdownRow title={t('gasPrice')}> |
||||
<CurrencyDisplay |
||||
className="transaction-breakdown__value" |
||||
currency={ETH} |
||||
denomination={GWEI} |
||||
value={gasPrice} |
||||
hideLabel |
||||
/> |
||||
</TransactionBreakdownRow> |
||||
<TransactionBreakdownRow title={t('total')}> |
||||
<div> |
||||
<CurrencyDisplay |
||||
className="transaction-breakdown__value transaction-breakdown__value--eth-total" |
||||
currency={ETH} |
||||
value={totalInHex} |
||||
numberOfDecimals={6} |
||||
/> |
||||
<CurrencyDisplay |
||||
className="transaction-breakdown__value" |
||||
value={totalInHex} |
||||
/> |
||||
</div> |
||||
</TransactionBreakdownRow> |
||||
</Card> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './transaction-list-item-details.component' |
@ -0,0 +1,49 @@ |
||||
.transaction-list-item-details { |
||||
&__header { |
||||
margin-bottom: 8px; |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
} |
||||
|
||||
&__header-buttons { |
||||
display: flex; |
||||
flex-direction: row; |
||||
} |
||||
|
||||
&__header-button { |
||||
font-size: .625rem; |
||||
|
||||
&:not(:last-child) { |
||||
margin-right: 8px; |
||||
} |
||||
} |
||||
|
||||
&__sender-to-recipient-container { |
||||
margin-bottom: 8px; |
||||
} |
||||
|
||||
&__cards-container { |
||||
display: flex; |
||||
flex-direction: row; |
||||
|
||||
@media screen and (max-width: $break-small) { |
||||
flex-direction: column; |
||||
} |
||||
} |
||||
|
||||
&__transaction-breakdown { |
||||
flex: 1; |
||||
margin-right: 8px; |
||||
min-width: 0; |
||||
|
||||
@media screen and (max-width: $break-small) { |
||||
margin: 0 0 8px 0; |
||||
} |
||||
} |
||||
|
||||
&__transaction-activity-log { |
||||
flex: 2; |
||||
min-width: 0; |
||||
} |
||||
} |
@ -0,0 +1,66 @@ |
||||
import React from 'react' |
||||
import assert from 'assert' |
||||
import { shallow } from 'enzyme' |
||||
import TransactionListItemDetails from '../transaction-list-item-details.component' |
||||
import Button from '../../button' |
||||
import SenderToRecipient from '../../sender-to-recipient' |
||||
import TransactionBreakdown from '../../transaction-breakdown' |
||||
import TransactionActivityLog from '../../transaction-activity-log' |
||||
|
||||
describe('TransactionListItemDetails Component', () => { |
||||
it('should render properly', () => { |
||||
const transaction = { |
||||
history: [], |
||||
id: 1, |
||||
status: 'confirmed', |
||||
txParams: { |
||||
from: '0x1', |
||||
gas: '0x5208', |
||||
gasPrice: '0x3b9aca00', |
||||
nonce: '0xa4', |
||||
to: '0x2', |
||||
value: '0x2386f26fc10000', |
||||
}, |
||||
} |
||||
|
||||
const wrapper = shallow( |
||||
<TransactionListItemDetails |
||||
transaction={transaction} |
||||
/>, |
||||
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } } |
||||
) |
||||
|
||||
assert.ok(wrapper.hasClass('transaction-list-item-details')) |
||||
assert.equal(wrapper.find(Button).length, 1) |
||||
assert.equal(wrapper.find(SenderToRecipient).length, 1) |
||||
assert.equal(wrapper.find(TransactionBreakdown).length, 1) |
||||
assert.equal(wrapper.find(TransactionActivityLog).length, 1) |
||||
}) |
||||
|
||||
it('should render a retry button', () => { |
||||
const transaction = { |
||||
history: [], |
||||
id: 1, |
||||
status: 'confirmed', |
||||
txParams: { |
||||
from: '0x1', |
||||
gas: '0x5208', |
||||
gasPrice: '0x3b9aca00', |
||||
nonce: '0xa4', |
||||
to: '0x2', |
||||
value: '0x2386f26fc10000', |
||||
}, |
||||
} |
||||
|
||||
const wrapper = shallow( |
||||
<TransactionListItemDetails |
||||
transaction={transaction} |
||||
showRetry={true} |
||||
/>, |
||||
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } } |
||||
) |
||||
|
||||
assert.ok(wrapper.hasClass('transaction-list-item-details')) |
||||
assert.equal(wrapper.find(Button).length, 2) |
||||
}) |
||||
}) |
@ -0,0 +1,80 @@ |
||||
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 TransactionActivityLog from '../transaction-activity-log' |
||||
import TransactionBreakdown from '../transaction-breakdown' |
||||
import Button from '../button' |
||||
import prefixForNetwork from '../../../lib/etherscan-prefix-for-network' |
||||
|
||||
export default class TransactionListItemDetails extends PureComponent { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
transaction: PropTypes.object, |
||||
showRetry: PropTypes.bool, |
||||
} |
||||
|
||||
handleEtherscanClick = () => { |
||||
const { hash, metamaskNetworkId } = this.props.transaction |
||||
|
||||
const prefix = prefixForNetwork(metamaskNetworkId) |
||||
const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}` |
||||
global.platform.openWindow({ url: etherscanUrl }) |
||||
this.setState({ showTransactionDetails: true }) |
||||
} |
||||
|
||||
render () { |
||||
const { t } = this.context |
||||
const { transaction, showRetry } = this.props |
||||
const { txParams: { to, from } = {} } = transaction |
||||
|
||||
return ( |
||||
<div className="transaction-list-item-details"> |
||||
<div className="transaction-list-item-details__header"> |
||||
<div>Details</div> |
||||
<div className="transaction-list-item-details__header-buttons"> |
||||
{ |
||||
showRetry && ( |
||||
<Button |
||||
type="raised" |
||||
onClick={this.handleEtherscanClick} |
||||
className="transaction-list-item-details__header-button" |
||||
> |
||||
{ t('speedUp') } |
||||
</Button> |
||||
) |
||||
} |
||||
<Button |
||||
type="raised" |
||||
onClick={this.handleEtherscanClick} |
||||
className="transaction-list-item-details__header-button" |
||||
> |
||||
<img src="/images/arrow-popout.svg" /> |
||||
</Button> |
||||
</div> |
||||
</div> |
||||
<div className="transaction-list-item-details__sender-to-recipient-container"> |
||||
<SenderToRecipient |
||||
variant={CARDS_VARIANT} |
||||
addressOnly |
||||
recipientAddress={to} |
||||
senderAddress={from} |
||||
/> |
||||
</div> |
||||
<div className="transaction-list-item-details__cards-container"> |
||||
<TransactionBreakdown |
||||
transaction={transaction} |
||||
className="transaction-list-item-details__transaction-breakdown" |
||||
/> |
||||
<TransactionActivityLog |
||||
transaction={transaction} |
||||
className="transaction-list-item-details__transaction-activity-log" |
||||
/> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -1 +1,3 @@ |
||||
export const ETH = 'ETH' |
||||
export const GWEI = 'GWEI' |
||||
export const WEI = 'WEI' |
||||
|
Loading…
Reference in new issue