// Copyright 2015, 2016 Ethcore (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Parity is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Parity. If not, see . import BigNumber from 'bignumber.js'; import React, { Component, PropTypes } from 'react'; import classnames from 'classnames'; import { api } from '../parity'; import styles from './transaction.css'; import IdentityIcon from '../../githubhint/IdentityIcon'; class BaseTransaction extends Component { shortHash (hash) { return `${hash.substr(0, 5)}..${hash.substr(hash.length - 3)}`; } renderHash (hash) { return ( { this.shortHash(hash) } ); } renderFrom (transaction) { if (!transaction) { return '-'; } return (
0x{ transaction.nonce.toString(16) }
); } renderGasPrice (transaction) { if (!transaction) { return '-'; } return ( { api.util.fromWei(transaction.gasPrice, 'shannon').toFormat(2) } shannon ); } renderGas (transaction) { if (!transaction) { return '-'; } return ( { transaction.gas.div(10 ** 6).toFormat(3) } MGas ); } renderPropagation (stats) { const noOfPeers = Object.keys(stats.propagatedTo).length; const noOfPropagations = Object.values(stats.propagatedTo).reduce((sum, val) => sum + val, 0); return ( { noOfPropagations } ({ noOfPeers } peers) ); } } export class Transaction extends BaseTransaction { static propTypes = { idx: PropTypes.number.isRequired, transaction: PropTypes.object.isRequired, blockNumber: PropTypes.object.isRequired, isLocal: PropTypes.bool, stats: PropTypes.object }; static defaultProps = { isLocal: false, stats: { firstSeen: 0, propagatedTo: {} } }; static renderHeader () { return ( Transaction From Gas Price Gas First propagation # Propagated ); } render () { const { isLocal, stats, transaction, idx } = this.props; const blockNo = new BigNumber(stats.firstSeen); const clazz = classnames(styles.transaction, { [styles.local]: isLocal }); return ( { idx }. { this.renderHash(transaction.hash) } { this.renderFrom(transaction) } { this.renderGasPrice(transaction) } { this.renderGas(transaction) } { this.renderTime(stats.firstSeen) } { this.renderPropagation(stats) } ); } renderTime (firstSeen) { const { blockNumber } = this.props; if (!firstSeen) { return 'never'; } const timeInMinutes = blockNumber.sub(firstSeen).mul(14).div(60).toFormat(1); return `${timeInMinutes} minutes ago`; } } export class LocalTransaction extends BaseTransaction { static propTypes = { hash: PropTypes.string.isRequired, status: PropTypes.string.isRequired, transaction: PropTypes.object, isLocal: PropTypes.bool, stats: PropTypes.object, details: PropTypes.object }; static defaultProps = { stats: { propagatedTo: {} } }; static renderHeader () { return ( Transaction From Gas Price / Gas Status ); } state = { isSending: false, isResubmitting: false, gasPrice: null, gas: null }; toggleResubmit = () => { const { transaction } = this.props; const { isResubmitting, gasPrice } = this.state; this.setState({ isResubmitting: !isResubmitting }); if (gasPrice === null) { this.setState({ gasPrice: `0x${transaction.gasPrice.toString(16)}`, gas: `0x${transaction.gas.toString(16)}` }); } }; setGasPrice = el => { this.setState({ gasPrice: el.target.value }); }; setGas = el => { this.setState({ gas: el.target.value }); }; sendTransaction = () => { const { transaction } = this.props; const { gasPrice, gas } = this.state; const newTransaction = { from: transaction.from, to: transaction.to, nonce: transaction.nonce, value: transaction.value, data: transaction.data, gasPrice, gas }; this.setState({ isResubmitting: false, isSending: true }); const closeSending = () => this.setState({ isSending: false, gasPrice: null, gas: null }); api.eth.sendTransaction(newTransaction) .then(closeSending) .catch(closeSending); }; render () { if (this.state.isResubmitting) { return this.renderResubmit(); } const { stats, transaction, hash, status } = this.props; const { isSending } = this.state; const resubmit = isSending ? ( 'sending...' ) : ( resubmit ); return ( { !transaction ? null : resubmit } { this.renderHash(hash) } { this.renderFrom(transaction) } { this.renderGasPrice(transaction) }
{ this.renderGas(transaction) } { this.renderStatus() }
{ status === 'pending' ? this.renderPropagation(stats) : null } ); } renderStatus () { const { details } = this.props; let state = { 'pending': () => 'In queue: Pending', 'future': () => 'In queue: Future', 'mined': () => 'Mined', 'dropped': () => 'Dropped because of queue limit', 'invalid': () => 'Transaction is invalid', 'rejected': () => `Rejected: ${details.error}`, 'replaced': () => `Replaced by ${this.shortHash(details.hash)}` }[this.props.status]; return state ? state() : 'unknown'; } // TODO [ToDr] Gas Price / Gas selection is not needed // when signer supports gasPrice/gas tunning. renderResubmit () { const { transaction } = this.props; const { gasPrice, gas } = this.state; return ( cancel { this.renderHash(transaction.hash) } { this.renderFrom(transaction) } Send ); } }