diff --git a/README.md b/README.md index 09bce8cb..8b197644 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # MyCrypto Beta (VISIT [MyCryptoHQ/mycrypto.com](https://github.com/MyCryptoHQ/mycrypto.com) for the current site)
Just looking to download? Grab our [latest release](https://github.com/MyCryptoHQ/MyCrypto/releases) [![Greenkeeper badge](https://badges.greenkeeper.io/MyCryptoHq/MyCrypto.svg)](https://greenkeeper.io/) +[![Coverage Status](https://coveralls.io/repos/github/MyCryptoHQ/MyCrypto/badge.svg?branch=develop)](https://coveralls.io/github/MyCryptoHQ/MyCrypto?branch=develop) ## Running the App diff --git a/common/Root.tsx b/common/Root.tsx index 0de08d6f..572cae7d 100644 --- a/common/Root.tsx +++ b/common/Root.tsx @@ -9,6 +9,7 @@ import SendTransaction from 'containers/Tabs/SendTransaction'; import Swap from 'containers/Tabs/Swap'; import SignAndVerifyMessage from 'containers/Tabs/SignAndVerifyMessage'; import BroadcastTx from 'containers/Tabs/BroadcastTx'; +import CheckTransaction from 'containers/Tabs/CheckTransaction'; import ErrorScreen from 'components/ErrorScreen'; import PageNotFound from 'components/PageNotFound'; import LogOutPrompt from 'components/LogOutPrompt'; @@ -67,6 +68,7 @@ export default class Root extends Component { + @@ -120,8 +122,7 @@ const LegacyRoutes = withRouter(props => { history.push('/account/info'); break; case '#check-tx-status': - history.push('/check-tx-status'); - break; + return ; } } diff --git a/common/actions/gas/actionCreators.ts b/common/actions/gas/actionCreators.ts new file mode 100644 index 00000000..c09b1bfe --- /dev/null +++ b/common/actions/gas/actionCreators.ts @@ -0,0 +1,19 @@ +import * as interfaces from './actionTypes'; +import { TypeKeys } from './constants'; + +export type TFetchGasEstimates = typeof fetchGasEstimates; +export function fetchGasEstimates(): interfaces.FetchGasEstimatesAction { + return { + type: TypeKeys.GAS_FETCH_ESTIMATES + }; +} + +export type TSetGasEstimates = typeof setGasEstimates; +export function setGasEstimates( + payload: interfaces.SetGasEstimatesAction['payload'] +): interfaces.SetGasEstimatesAction { + return { + type: TypeKeys.GAS_SET_ESTIMATES, + payload + }; +} diff --git a/common/actions/gas/actionTypes.ts b/common/actions/gas/actionTypes.ts new file mode 100644 index 00000000..a66b86c0 --- /dev/null +++ b/common/actions/gas/actionTypes.ts @@ -0,0 +1,14 @@ +import { TypeKeys } from './constants'; +import { GasEstimates } from 'api/gas'; + +export interface FetchGasEstimatesAction { + type: TypeKeys.GAS_FETCH_ESTIMATES; +} + +export interface SetGasEstimatesAction { + type: TypeKeys.GAS_SET_ESTIMATES; + payload: GasEstimates; +} + +/*** Union Type ***/ +export type GasAction = FetchGasEstimatesAction | SetGasEstimatesAction; diff --git a/common/actions/gas/constants.ts b/common/actions/gas/constants.ts new file mode 100644 index 00000000..569bcb6b --- /dev/null +++ b/common/actions/gas/constants.ts @@ -0,0 +1,4 @@ +export enum TypeKeys { + GAS_FETCH_ESTIMATES = 'GAS_FETCH_ESTIMATES', + GAS_SET_ESTIMATES = 'GAS_SET_ESTIMATES' +} diff --git a/common/actions/gas/index.ts b/common/actions/gas/index.ts new file mode 100644 index 00000000..51fcd517 --- /dev/null +++ b/common/actions/gas/index.ts @@ -0,0 +1,3 @@ +export * from './actionCreators'; +export * from './actionTypes'; +export * from './constants'; diff --git a/common/actions/transactions/actionCreators.ts b/common/actions/transactions/actionCreators.ts new file mode 100644 index 00000000..30e2ff95 --- /dev/null +++ b/common/actions/transactions/actionCreators.ts @@ -0,0 +1,20 @@ +import * as interfaces from './actionTypes'; +import { TypeKeys } from './constants'; + +export type TFetchTransactionData = typeof fetchTransactionData; +export function fetchTransactionData(txhash: string): interfaces.FetchTransactionDataAction { + return { + type: TypeKeys.TRANSACTIONS_FETCH_TRANSACTION_DATA, + payload: txhash + }; +} + +export type TSetTransactionData = typeof setTransactionData; +export function setTransactionData( + payload: interfaces.SetTransactionDataAction['payload'] +): interfaces.SetTransactionDataAction { + return { + type: TypeKeys.TRANSACTIONS_SET_TRANSACTION_DATA, + payload + }; +} diff --git a/common/actions/transactions/actionTypes.ts b/common/actions/transactions/actionTypes.ts new file mode 100644 index 00000000..ee4380b3 --- /dev/null +++ b/common/actions/transactions/actionTypes.ts @@ -0,0 +1,20 @@ +import { TypeKeys } from './constants'; +import { TransactionData, TransactionReceipt } from 'libs/nodes'; + +export interface FetchTransactionDataAction { + type: TypeKeys.TRANSACTIONS_FETCH_TRANSACTION_DATA; + payload: string; +} + +export interface SetTransactionDataAction { + type: TypeKeys.TRANSACTIONS_SET_TRANSACTION_DATA; + payload: { + txhash: string; + data: TransactionData | null; + receipt: TransactionReceipt | null; + error: string | null; + }; +} + +/*** Union Type ***/ +export type TransactionsAction = FetchTransactionDataAction | SetTransactionDataAction; diff --git a/common/actions/transactions/constants.ts b/common/actions/transactions/constants.ts new file mode 100644 index 00000000..cbd8ad82 --- /dev/null +++ b/common/actions/transactions/constants.ts @@ -0,0 +1,5 @@ +export enum TypeKeys { + TRANSACTIONS_FETCH_TRANSACTION_DATA = 'TRANSACTIONS_FETCH_TRANSACTION_DATA', + TRANSACTIONS_SET_TRANSACTION_DATA = 'TRANSACTIONS_SET_TRANSACTION_DATA', + TRANSACTIONS_SET_TRANSACTION_ERROR = 'TRANSACTIONS_SET_TRANSACTION_ERROR' +} diff --git a/common/actions/transactions/index.ts b/common/actions/transactions/index.ts new file mode 100644 index 00000000..51fcd517 --- /dev/null +++ b/common/actions/transactions/index.ts @@ -0,0 +1,3 @@ +export * from './actionCreators'; +export * from './actionTypes'; +export * from './constants'; diff --git a/common/api/gas.ts b/common/api/gas.ts new file mode 100644 index 00000000..d61df701 --- /dev/null +++ b/common/api/gas.ts @@ -0,0 +1,71 @@ +import { checkHttpStatus, parseJSON } from './utils'; + +const MAX_GAS_FAST = 250; + +interface RawGasEstimates { + safeLow: number; + standard: number; + fast: number; + fastest: number; + block_time: number; + blockNum: number; +} + +export interface GasEstimates { + safeLow: number; + standard: number; + fast: number; + fastest: number; + time: number; + isDefault: boolean; +} + +export function fetchGasEstimates(): Promise { + return fetch('https://dev.blockscale.net/api/gasexpress.json', { + mode: 'cors' + }) + .then(checkHttpStatus) + .then(parseJSON) + .then((res: object) => { + // Make sure it looks like a raw gas estimate, and it has valid values + const keys = ['safeLow', 'standard', 'fast', 'fastest']; + keys.forEach(key => { + if (typeof res[key] !== 'number') { + throw new Error( + `Gas estimate API has invalid shape: Expected numeric key '${key}' in response, got '${ + res[key] + }' instead` + ); + } + }); + + // Make sure the estimate isn't totally crazy + const estimateRes = res as RawGasEstimates; + if (estimateRes.fast > MAX_GAS_FAST) { + throw new Error( + `Gas estimate response estimate too high: Max fast is ${MAX_GAS_FAST}, was given ${ + estimateRes.fast + }` + ); + } + + if ( + estimateRes.safeLow > estimateRes.standard || + estimateRes.standard > estimateRes.fast || + estimateRes.fast > estimateRes.fastest + ) { + throw new Error( + `Gas esimates are in illogical order: should be safeLow < standard < fast < fastest, received ${ + estimateRes.safeLow + } < ${estimateRes.standard} < ${estimateRes.fast} < ${estimateRes.fastest}` + ); + } + + return estimateRes; + }) + .then((res: RawGasEstimates) => ({ + ...res, + time: Date.now(), + isDefault: false + })); +} diff --git a/common/components/BalanceSidebar/AccountInfo.tsx b/common/components/BalanceSidebar/AccountInfo.tsx index c5f791ec..8c2c5a88 100644 --- a/common/components/BalanceSidebar/AccountInfo.tsx +++ b/common/components/BalanceSidebar/AccountInfo.tsx @@ -134,17 +134,21 @@ class AccountInfo extends React.Component { symbol={balance.wei ? network.name : null} /> - {balance.isPending ? ( - - ) : ( - !isOffline && ( - - ) + {balance.wei && ( + + {balance.isPending ? ( + + ) : ( + !isOffline && ( + + ) + )} + )} diff --git a/common/components/BalanceSidebar/PromoComponents/Bity.tsx b/common/components/BalanceSidebar/PromoComponents/Bity.tsx deleted file mode 100644 index fad931fd..00000000 --- a/common/components/BalanceSidebar/PromoComponents/Bity.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; -import BityLogo from 'assets/images/logo-bity-white.svg'; - -export const Bity: React.SFC = () => ( - -
-
-

It’s now easier to get more ETH

-
Swap BTC <-> ETH
-
-
- -
-
- -); diff --git a/common/components/BalanceSidebar/PromoComponents/Coinbase.tsx b/common/components/BalanceSidebar/PromoComponents/Coinbase.tsx index db253f92..c3714652 100644 --- a/common/components/BalanceSidebar/PromoComponents/Coinbase.tsx +++ b/common/components/BalanceSidebar/PromoComponents/Coinbase.tsx @@ -1,11 +1,10 @@ import React from 'react'; import CoinbaseLogo from 'assets/images/logo-coinbase.svg'; +import { NewTabLink } from 'components/ui'; export const Coinbase: React.SFC = () => ( -
@@ -17,5 +16,5 @@ export const Coinbase: React.SFC = () => (
-
+ ); diff --git a/common/components/BalanceSidebar/PromoComponents/HardwareWallets.tsx b/common/components/BalanceSidebar/PromoComponents/HardwareWallets.tsx index 45b667e8..a9dd2440 100644 --- a/common/components/BalanceSidebar/PromoComponents/HardwareWallets.tsx +++ b/common/components/BalanceSidebar/PromoComponents/HardwareWallets.tsx @@ -5,10 +5,7 @@ import ledgerLogo from 'assets/images/logo-ledger.svg'; import trezorLogo from 'assets/images/logo-trezor.svg'; export const HardwareWallets: React.SFC = () => ( - +
Learn more about protecting your funds.
diff --git a/common/components/BalanceSidebar/PromoComponents/Shapeshift.tsx b/common/components/BalanceSidebar/PromoComponents/Shapeshift.tsx new file mode 100644 index 00000000..dca7b09b --- /dev/null +++ b/common/components/BalanceSidebar/PromoComponents/Shapeshift.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import ShapeshiftLogo from 'assets/images/logo-shapeshift.svg'; + +export const Shapeshift: React.SFC = () => ( + +
+
+
+ Exchange Coins +
+ & Tokens with +
+
+
+ +
+
+ +); diff --git a/common/components/BalanceSidebar/PromoComponents/index.ts b/common/components/BalanceSidebar/PromoComponents/index.ts index 31aec6fc..358c3f4d 100644 --- a/common/components/BalanceSidebar/PromoComponents/index.ts +++ b/common/components/BalanceSidebar/PromoComponents/index.ts @@ -1,3 +1,3 @@ export * from './HardwareWallets'; export * from './Coinbase'; -export * from './Bity'; +export * from './Shapeshift'; diff --git a/common/components/BalanceSidebar/Promos.scss b/common/components/BalanceSidebar/Promos.scss index 8ce3d216..505dda6c 100644 --- a/common/components/BalanceSidebar/Promos.scss +++ b/common/components/BalanceSidebar/Promos.scss @@ -9,18 +9,6 @@ overflow: hidden; } - &-Bity { - background-color: #006e79; - } - - &-Coinbase { - background-color: #2b71b1; - } - - &-HardwareWallets { - background-color: #6e9a3e; - } - &-promo { position: relative; height: inherit; @@ -42,19 +30,18 @@ position: absolute; display: flex; align-items: center; + justify-content: center; top: 50%; left: 0; width: 100%; transform: translateY(-50%); - } - - &-text, - &-images { - padding: 0 $space-sm; + padding: 0 $space; } &-text { - flex: 1; + flex: 5; + padding-right: $space-xs; + max-width: 220px; p, h4, @@ -73,15 +60,15 @@ } &-images { - padding: 0 $space * 1.5; + flex: 3; + max-width: 108px; + padding-left: $space-xs; img { display: block; margin: 0 auto; width: 100%; - max-width: 96px; height: auto; - padding: $space-xs; } } } @@ -106,6 +93,23 @@ } } } + + // Per-promo customizations + &--shapeshift { + background-color: #263A52; + + .Promos-promo-images { + max-width: 130px; + } + } + + &--coinbase { + background-color: #2b71b1; + } + + &--hardware { + background-color: #6e9a3e; + } } .carousel-exit { diff --git a/common/components/BalanceSidebar/Promos.tsx b/common/components/BalanceSidebar/Promos.tsx index 9314ff61..32f1fbfe 100644 --- a/common/components/BalanceSidebar/Promos.tsx +++ b/common/components/BalanceSidebar/Promos.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { TransitionGroup, CSSTransition } from 'react-transition-group'; -import { HardwareWallets, Coinbase, Bity } from './PromoComponents'; +import { HardwareWallets, Coinbase, Shapeshift } from './PromoComponents'; import './Promos.scss'; -const promos = [HardwareWallets, Coinbase, Bity]; +const promos = [HardwareWallets, Coinbase, Shapeshift]; const CarouselAnimation = ({ children, ...props }) => ( @@ -36,11 +36,7 @@ export default class Promos extends React.PureComponent<{}, State> { return (
- {promos - .filter(i => { - return i === promos[activePromo]; - }) - .map(promo => {promo})} + {promos[activePromo]}
{promos.map((_, index) => { diff --git a/common/components/CurrentCustomMessage.tsx b/common/components/CurrentCustomMessage.tsx index 0dee62c3..66c16070 100644 --- a/common/components/CurrentCustomMessage.tsx +++ b/common/components/CurrentCustomMessage.tsx @@ -13,19 +13,24 @@ interface ReduxProps { wallet: AppState['wallet']['inst']; } +type Props = ReduxProps; + interface State { walletAddress: string | null; } -class CurrentCustomMessageClass extends PureComponent { +class CurrentCustomMessageClass extends PureComponent { public state: State = { walletAddress: null }; public async componentDidMount() { - if (this.props.wallet) { - const walletAddress = await this.props.wallet.getAddressString(); - this.setState({ walletAddress }); + this.setAddressState(this.props); + } + + public componentWillReceiveProps(nextProps: Props) { + if (this.props.wallet !== nextProps.wallet) { + this.setAddressState(nextProps); } } @@ -42,6 +47,15 @@ class CurrentCustomMessageClass extends PureComponent { } } + private async setAddressState(props: Props) { + if (props.wallet) { + const walletAddress = await props.wallet.getAddressString(); + this.setState({ walletAddress }); + } else { + this.setState({ walletAddress: '' }); + } + } + private getMessage() { const { currentTo, tokens } = this.props; const { walletAddress } = this.state; diff --git a/common/components/Header/components/GasPriceDropdown.tsx b/common/components/Header/components/GasPriceDropdown.tsx index d4b1fd94..dc355042 100644 --- a/common/components/Header/components/GasPriceDropdown.tsx +++ b/common/components/Header/components/GasPriceDropdown.tsx @@ -53,8 +53,8 @@ class GasPriceDropdown extends Component {

Not So Fast

diff --git a/common/components/Header/components/Navigation.tsx b/common/components/Header/components/Navigation.tsx index 2695082f..35321754 100644 --- a/common/components/Header/components/Navigation.tsx +++ b/common/components/Header/components/Navigation.tsx @@ -34,6 +34,10 @@ const tabs: TabLink[] = [ name: 'Sign & Verify Message', to: '/sign-and-verify-message' }, + { + name: 'NAV_TxStatus', + to: '/tx-status' + }, { name: 'Broadcast Transaction', to: '/pushTx' diff --git a/common/components/TXMetaDataPanel/components/FeeSummary.tsx b/common/components/TXMetaDataPanel/components/FeeSummary.tsx index 0dcd532e..615162a2 100644 --- a/common/components/TXMetaDataPanel/components/FeeSummary.tsx +++ b/common/components/TXMetaDataPanel/components/FeeSummary.tsx @@ -3,7 +3,9 @@ import BN from 'bn.js'; import { connect } from 'react-redux'; import { AppState } from 'reducers'; import { getNetworkConfig, getOffline } from 'selectors/config'; -import { UnitDisplay } from 'components/ui'; +import { getIsEstimating } from 'selectors/gas'; +import { getGasLimit } from 'selectors/transaction'; +import { UnitDisplay, Spinner } from 'components/ui'; import { NetworkConfig } from 'types/network'; import './FeeSummary.scss'; @@ -20,6 +22,7 @@ interface ReduxStateProps { rates: AppState['rates']['rates']; network: NetworkConfig; isOffline: AppState['config']['meta']['offline']; + isGasEstimating: AppState['gas']['isEstimating']; } interface OwnProps { @@ -31,7 +34,15 @@ type Props = OwnProps & ReduxStateProps; class FeeSummary extends React.Component { public render() { - const { gasPrice, gasLimit, rates, network, isOffline } = this.props; + const { gasPrice, gasLimit, rates, network, isOffline, isGasEstimating } = this.props; + + if (isGasEstimating) { + return ( +
+ +
+ ); + } const feeBig = gasPrice.value && gasLimit.value && gasPrice.value.mul(gasLimit.value); const fee = ( @@ -73,10 +84,11 @@ class FeeSummary extends React.Component { function mapStateToProps(state: AppState): ReduxStateProps { return { - gasLimit: state.transaction.fields.gasLimit, + gasLimit: getGasLimit(state), rates: state.rates.rates, network: getNetworkConfig(state), - isOffline: getOffline(state) + isOffline: getOffline(state), + isGasEstimating: getIsEstimating(state) }; } diff --git a/common/components/TXMetaDataPanel/components/SimpleGas.tsx b/common/components/TXMetaDataPanel/components/SimpleGas.tsx index 7dd88361..6ae51845 100644 --- a/common/components/TXMetaDataPanel/components/SimpleGas.tsx +++ b/common/components/TXMetaDataPanel/components/SimpleGas.tsx @@ -11,33 +11,50 @@ import { nonceRequestPending } from 'selectors/transaction'; import { connect } from 'react-redux'; +import { fetchGasEstimates, TFetchGasEstimates } from 'actions/gas'; import { getIsWeb3Node } from 'selectors/config'; +import { getEstimates, getIsEstimating } from 'selectors/gas'; import { Wei, fromWei } from 'libs/units'; import { InlineSpinner } from 'components/ui/InlineSpinner'; const SliderWithTooltip = Slider.createSliderWithTooltip(Slider); interface OwnProps { gasPrice: AppState['transaction']['fields']['gasPrice']; - noncePending: boolean; - gasLimitPending: boolean; inputGasPrice(rawGas: string); setGasPrice(rawGas: string); } interface StateProps { + gasEstimates: AppState['gas']['estimates']; + isGasEstimating: AppState['gas']['isEstimating']; + noncePending: boolean; + gasLimitPending: boolean; isWeb3Node: boolean; gasLimitEstimationTimedOut: boolean; } -type Props = OwnProps & StateProps; +interface ActionProps { + fetchGasEstimates: TFetchGasEstimates; +} + +type Props = OwnProps & StateProps & ActionProps; class SimpleGas extends React.Component { public componentDidMount() { this.fixGasPrice(this.props.gasPrice); + this.props.fetchGasEstimates(); + } + + public componentWillReceiveProps(nextProps: Props) { + if (!this.props.gasEstimates && nextProps.gasEstimates) { + this.props.setGasPrice(nextProps.gasEstimates.fast.toString()); + } } public render() { const { + isGasEstimating, + gasEstimates, gasPrice, gasLimitEstimationTimedOut, isWeb3Node, @@ -45,6 +62,11 @@ class SimpleGas extends React.Component { gasLimitPending } = this.props; + const bounds = { + max: gasEstimates ? gasEstimates.fastest : gasPriceDefaults.minGwei, + min: gasEstimates ? gasEstimates.safeLow : gasPriceDefaults.maxGwei + }; + return (
@@ -69,14 +91,14 @@ class SimpleGas extends React.Component {
`${gas} Gwei`} + tipFormatter={this.formatTooltip} + disabled={isGasEstimating} />
{translate('Cheap')} - {translate('Balanced')} {translate('Fast')}
@@ -100,21 +122,38 @@ class SimpleGas extends React.Component { private fixGasPrice(gasPrice: AppState['transaction']['fields']['gasPrice']) { // If the gas price is above or below our minimum, bring it in line const gasPriceGwei = this.getGasPriceGwei(gasPrice.value); - if (gasPriceGwei > gasPriceDefaults.gasPriceMaxGwei) { - this.props.setGasPrice(gasPriceDefaults.gasPriceMaxGwei.toString()); - } else if (gasPriceGwei < gasPriceDefaults.gasPriceMinGwei) { - this.props.setGasPrice(gasPriceDefaults.gasPriceMinGwei.toString()); + if (gasPriceGwei > gasPriceDefaults.maxGwei) { + this.props.setGasPrice(gasPriceDefaults.maxGwei.toString()); + } else if (gasPriceGwei < gasPriceDefaults.minGwei) { + this.props.setGasPrice(gasPriceDefaults.minGwei.toString()); } } private getGasPriceGwei(gasPriceValue: Wei) { return parseFloat(fromWei(gasPriceValue, 'gwei')); } + + private formatTooltip = (gas: number) => { + const { gasEstimates } = this.props; + let recommended = ''; + if (gasEstimates && !gasEstimates.isDefault && gas === gasEstimates.fast) { + recommended = '(Recommended)'; + } + + return `${gas} Gwei ${recommended}`; + }; } -export default connect((state: AppState) => ({ - noncePending: nonceRequestPending(state), - gasLimitPending: getGasEstimationPending(state), - gasLimitEstimationTimedOut: getGasLimitEstimationTimedOut(state), - isWeb3Node: getIsWeb3Node(state) -}))(SimpleGas); +export default connect( + (state: AppState): StateProps => ({ + gasEstimates: getEstimates(state), + isGasEstimating: getIsEstimating(state), + noncePending: nonceRequestPending(state), + gasLimitPending: getGasEstimationPending(state), + gasLimitEstimationTimedOut: getGasLimitEstimationTimedOut(state), + isWeb3Node: getIsWeb3Node(state) + }), + { + fetchGasEstimates + } +)(SimpleGas); diff --git a/common/components/TransactionStatus/TransactionDataTable.scss b/common/components/TransactionStatus/TransactionDataTable.scss new file mode 100644 index 00000000..be38bb82 --- /dev/null +++ b/common/components/TransactionStatus/TransactionDataTable.scss @@ -0,0 +1,49 @@ +@import 'common/sass/variables'; +@import 'common/sass/mixins'; + +.TxData { + &-row { + font-size: 0.9rem; + + &-label { + font-weight: bold; + text-align: right; + } + + &-data { + @include mono; + + &-more { + margin-left: $space-sm; + font-size: 0.7rem; + opacity: 0.7; + } + + &-status { + font-weight: bold; + + &.is-success { + color: $brand-success; + } + &.is-warning { + color: $brand-warning; + } + &.is-danger { + color: $brand-danger; + } + } + + .Identicon { + float: left; + margin-right: $space-sm; + } + + textarea { + display: block; + width: 100%; + height: 7.2rem; + background: $gray-lighter; + } + } + } +} diff --git a/common/components/TransactionStatus/TransactionDataTable.tsx b/common/components/TransactionStatus/TransactionDataTable.tsx new file mode 100644 index 00000000..4e5d2a6c --- /dev/null +++ b/common/components/TransactionStatus/TransactionDataTable.tsx @@ -0,0 +1,174 @@ +import React from 'react'; +import translate from 'translations'; +import { Identicon, UnitDisplay, NewTabLink } from 'components/ui'; +import { TransactionData, TransactionReceipt } from 'libs/nodes'; +import { NetworkConfig } from 'types/network'; +import './TransactionDataTable.scss'; + +interface TableRow { + label: React.ReactElement | string; + data: React.ReactElement | string | number | null; +} + +const MaybeLink: React.SFC<{ + href: string | null | undefined | false; + children: any; // Too many damn React element types +}> = ({ href, children }) => { + if (href) { + return {children}; + } else { + return {children}; + } +}; + +interface Props { + data: TransactionData; + receipt: TransactionReceipt | null; + network: NetworkConfig; +} + +const TransactionDataTable: React.SFC = ({ data, receipt, network }) => { + const explorer: { [key: string]: string | false | null } = {}; + const hasInputData = data.input && data.input !== '0x'; + + if (!network.isCustom) { + explorer.tx = network.blockExplorer && network.blockExplorer.txUrl(data.hash); + explorer.block = + network.blockExplorer && + !!data.blockNumber && + network.blockExplorer.blockUrl(data.blockNumber); + explorer.to = network.blockExplorer && network.blockExplorer.addressUrl(data.to); + explorer.from = network.blockExplorer && network.blockExplorer.addressUrl(data.from); + explorer.contract = + network.blockExplorer && + receipt && + receipt.contractAddress && + network.blockExplorer.addressUrl(receipt.contractAddress); + } + + let statusMsg = ''; + let statusType = ''; + let statusSeeMore = false; + if (receipt) { + if (receipt.status === 1) { + statusMsg = 'SUCCESSFUL'; + statusType = 'success'; + } else if (receipt.status === 0) { + statusMsg = 'FAILED'; + statusType = 'danger'; + statusSeeMore = true; + } else { + // Pre-byzantium transactions don't use status, and cannot have their + // success determined over the JSON RPC api + statusMsg = 'UNKNOWN'; + statusType = 'warning'; + statusSeeMore = true; + } + } else { + statusMsg = 'PENDING'; + statusType = 'warning'; + } + + const rows: TableRow[] = [ + { + label: 'Status', + data: ( + + {statusMsg} + {statusSeeMore && + explorer.tx && + !network.isCustom && ( + + (See more on {network.blockExplorer.name}) + + )} + + ) + }, + { + label: translate('x_TxHash'), + data: {data.hash} + }, + { + label: 'Block Number', + data: receipt && {receipt.blockNumber} + }, + { + label: translate('OFFLINE_Step1_Label_1'), + data: ( + + + {data.from} + + ) + }, + { + label: translate('OFFLINE_Step2_Label_1'), + data: ( + + + {data.to} + + ) + }, + { + label: translate('SEND_amount_short'), + data: + }, + { + label: translate('OFFLINE_Step2_Label_3'), + data: + }, + { + label: translate('OFFLINE_Step2_Label_4'), + data: + }, + { + label: 'Gas Used', + data: receipt && + }, + { + label: 'Transaction Fee', + data: receipt && ( + + ) + }, + { + label: translate('New contract address'), + data: receipt && + receipt.contractAddress && ( + {receipt.contractAddress} + ) + }, + { + label: translate('OFFLINE_Step2_Label_5'), + data: data.nonce + }, + { + label: translate('TRANS_data'), + data: hasInputData ? ( +