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 ? (
+
+ ) : null
+ }
+ ];
+
+ const filteredRows = rows.filter(row => !!row.data);
+ return (
+
+
+ {filteredRows.map((row, idx) => (
+
+ {row.label} |
+ {row.data} |
+
+ ))}
+
+
+ );
+};
+
+export default TransactionDataTable;
diff --git a/common/components/TransactionStatus/TransactionStatus.scss b/common/components/TransactionStatus/TransactionStatus.scss
new file mode 100644
index 00000000..8bf4bd1f
--- /dev/null
+++ b/common/components/TransactionStatus/TransactionStatus.scss
@@ -0,0 +1,35 @@
+@import 'common/sass/variables';
+
+.TxStatus {
+ text-align: center;
+
+ &-title {
+ margin-top: 0;
+ }
+
+ &-data {
+ text-align: left;
+ }
+
+ &-loading {
+ padding: $space * 3 0;
+ }
+
+ &-error {
+ max-width: 620px;
+ margin: 0 auto;
+
+ &-title {
+ color: $brand-danger;
+ }
+
+ &-desc {
+ font-size: $font-size-bump;
+ margin-bottom: $space-md;
+ }
+
+ &-list {
+ text-align: left;
+ }
+ }
+}
diff --git a/common/components/TransactionStatus/TransactionStatus.tsx b/common/components/TransactionStatus/TransactionStatus.tsx
new file mode 100644
index 00000000..0c75d834
--- /dev/null
+++ b/common/components/TransactionStatus/TransactionStatus.tsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import translate from 'translations';
+import { fetchTransactionData, TFetchTransactionData } from 'actions/transactions';
+import { getTransactionDatas } from 'selectors/transactions';
+import { getNetworkConfig } from 'selectors/config';
+import { Spinner } from 'components/ui';
+import TransactionDataTable from './TransactionDataTable';
+import { AppState } from 'reducers';
+import { NetworkConfig } from 'types/network';
+import { TransactionState } from 'reducers/transactions';
+import './TransactionStatus.scss';
+
+interface OwnProps {
+ txHash: string;
+}
+
+interface StateProps {
+ tx: TransactionState | null;
+ network: NetworkConfig;
+}
+
+interface ActionProps {
+ fetchTransactionData: TFetchTransactionData;
+}
+
+type Props = OwnProps & StateProps & ActionProps;
+
+class TransactionStatus extends React.Component {
+ public componentDidMount() {
+ this.props.fetchTransactionData(this.props.txHash);
+ }
+
+ public componentWillReceiveProps(nextProps: Props) {
+ if (this.props.txHash !== nextProps.txHash) {
+ this.props.fetchTransactionData(nextProps.txHash);
+ }
+ }
+
+ public render() {
+ const { tx, network } = this.props;
+ let content;
+
+ if (tx && tx.data) {
+ content = (
+
+ Transaction Found
+
+
+
+
+ );
+ } else if (tx && tx.error) {
+ content = (
+
+
{translate('tx_notFound')}
+
{translate('tx_notFound_1')}
+
+ - Make sure you copied the Transaction Hash correctly
+ - {translate('tx_notFound_2')}
+ - {translate('tx_notFound_3')}
+ - {translate('tx_notFound_4')}
+
+
+ );
+ } else if (tx && tx.isLoading) {
+ // tx.isLoading... probably.
+ content = (
+
+
+
+ );
+ }
+
+ return {content}
;
+ }
+}
+
+function mapStateToProps(state: AppState, ownProps: OwnProps): StateProps {
+ const { txHash } = ownProps;
+
+ return {
+ tx: getTransactionDatas(state)[txHash],
+ network: getNetworkConfig(state)
+ };
+}
+
+export default connect(mapStateToProps, { fetchTransactionData })(TransactionStatus);
diff --git a/common/components/TransactionStatus/index.tsx b/common/components/TransactionStatus/index.tsx
new file mode 100644
index 00000000..ae3b19f8
--- /dev/null
+++ b/common/components/TransactionStatus/index.tsx
@@ -0,0 +1,2 @@
+import TransactionStatus from './TransactionStatus';
+export default TransactionStatus;
diff --git a/common/components/WalletDecrypt/components/InsecureWalletWarning.tsx b/common/components/WalletDecrypt/components/InsecureWalletWarning.tsx
index 8effe92e..8acd984f 100644
--- a/common/components/WalletDecrypt/components/InsecureWalletWarning.tsx
+++ b/common/components/WalletDecrypt/components/InsecureWalletWarning.tsx
@@ -114,7 +114,7 @@ export class InsecureWalletWarning extends React.Component {
private makeCheckbox = (checkbox: Checkbox) => {
return (
-