Show Recent Txs on Check Tx Page (#1147)
* Save transactions to local storage. * Checksum more things + reset hash on network change. * Fix IHexTransaction type, grab from from tx object directly. * Refactor storage of recent transactions to use redux storage and loading. * Refactor types to a transactions types file. * Initial crack at recent transactions tab on account * Punctuation. * Transaction Status responsive behavior. * Refactor transaction helper function out to remove circular dependency. * Fix typings * Collapse subtabs to select list when too small. * s/wallet/address * Type select onChange * Get fields from current state if web3 tx
This commit is contained in:
parent
740b191542
commit
db6b737cad
|
@ -18,3 +18,18 @@ export function setTransactionData(
|
|||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export type TResetTransactionData = typeof resetTransactionData;
|
||||
export function resetTransactionData(): interfaces.ResetTransactionDataAction {
|
||||
return { type: TypeKeys.TRANSACTIONS_RESET_TRANSACTION_DATA };
|
||||
}
|
||||
|
||||
export type TAddRecentTransaction = typeof addRecentTransaction;
|
||||
export function addRecentTransaction(
|
||||
payload: interfaces.AddRecentTransactionAction['payload']
|
||||
): interfaces.AddRecentTransactionAction {
|
||||
return {
|
||||
type: TypeKeys.TRANSACTIONS_ADD_RECENT_TRANSACTION,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { TypeKeys } from './constants';
|
||||
import { TransactionData, TransactionReceipt } from 'libs/nodes';
|
||||
import { SavedTransaction, TransactionData, TransactionReceipt } from 'types/transactions';
|
||||
|
||||
export interface FetchTransactionDataAction {
|
||||
type: TypeKeys.TRANSACTIONS_FETCH_TRANSACTION_DATA;
|
||||
|
@ -16,5 +16,18 @@ export interface SetTransactionDataAction {
|
|||
};
|
||||
}
|
||||
|
||||
export interface ResetTransactionDataAction {
|
||||
type: TypeKeys.TRANSACTIONS_RESET_TRANSACTION_DATA;
|
||||
}
|
||||
|
||||
export interface AddRecentTransactionAction {
|
||||
type: TypeKeys.TRANSACTIONS_ADD_RECENT_TRANSACTION;
|
||||
payload: SavedTransaction;
|
||||
}
|
||||
|
||||
/*** Union Type ***/
|
||||
export type TransactionsAction = FetchTransactionDataAction | SetTransactionDataAction;
|
||||
export type TransactionsAction =
|
||||
| FetchTransactionDataAction
|
||||
| SetTransactionDataAction
|
||||
| ResetTransactionDataAction
|
||||
| AddRecentTransactionAction;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
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'
|
||||
TRANSACTIONS_SET_TRANSACTION_ERROR = 'TRANSACTIONS_SET_TRANSACTION_ERROR',
|
||||
TRANSACTIONS_RESET_TRANSACTION_DATA = 'TRANSACTIONS_RESET_TRANSACTION_DATA',
|
||||
TRANSACTIONS_ADD_RECENT_TRANSACTION = 'TRANSACTIONS_ADD_RECENT_TRANSACTION'
|
||||
}
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
margin-top: 15px;
|
||||
|
||||
&-tabs {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
|
||||
&-link {
|
||||
display: inline-block;
|
||||
padding: 8px;
|
||||
|
@ -26,4 +29,8 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-select {
|
||||
margin-bottom: $space-md;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import Select, { Option } from 'react-select';
|
||||
import { NavLink, RouteComponentProps } from 'react-router-dom';
|
||||
import './SubTabs.scss';
|
||||
|
||||
|
@ -9,32 +10,135 @@ export interface Tab {
|
|||
redirect?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
interface OwnProps {
|
||||
tabs: Tab[];
|
||||
match: RouteComponentProps<{}>['match'];
|
||||
}
|
||||
|
||||
export default class SubTabs extends React.PureComponent<Props> {
|
||||
type Props = OwnProps & RouteComponentProps<{}>;
|
||||
|
||||
interface State {
|
||||
tabsWidth: number;
|
||||
isCollapsed: boolean;
|
||||
}
|
||||
|
||||
export default class SubTabs extends React.PureComponent<Props, State> {
|
||||
public state: State = {
|
||||
tabsWidth: 0,
|
||||
isCollapsed: false
|
||||
};
|
||||
|
||||
private containerEl: HTMLDivElement | null;
|
||||
private tabsEl: HTMLDivElement | null;
|
||||
|
||||
public componentDidMount() {
|
||||
this.measureTabsWidth();
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
// When new tabs come in, we'll need to uncollapse so that they can
|
||||
// be measured and collapsed again, if needed.
|
||||
if (this.props.tabs !== nextProps.tabs) {
|
||||
this.setState({ isCollapsed: false });
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Props) {
|
||||
// New tabs === new measurements
|
||||
if (this.props.tabs !== prevProps.tabs) {
|
||||
this.measureTabsWidth();
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { tabs, match } = this.props;
|
||||
const currentPath = match.url;
|
||||
const { isCollapsed } = this.state;
|
||||
const basePath = match.url;
|
||||
const currentPath = location.pathname;
|
||||
let content: React.ReactElement<string>;
|
||||
|
||||
return (
|
||||
<div className="SubTabs row">
|
||||
<div className="SubTabs-tabs col-sm-12">
|
||||
if (isCollapsed) {
|
||||
const options = tabs.map(tab => ({
|
||||
label: tab.name as string,
|
||||
value: tab.path,
|
||||
disabled: tab.disabled
|
||||
}));
|
||||
|
||||
content = (
|
||||
<div className="SubTabs-select">
|
||||
<Select
|
||||
options={options}
|
||||
value={currentPath.split('/').pop()}
|
||||
onChange={this.handleSelect}
|
||||
searchable={false}
|
||||
clearable={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// All tabs visible navigation
|
||||
content = (
|
||||
<div className="SubTabs-tabs" ref={el => (this.tabsEl = el)}>
|
||||
{tabs.map((t, i) => (
|
||||
// Same as normal Link, but knows when it's active, and applies activeClassName
|
||||
<NavLink
|
||||
className={`SubTabs-tabs-link ${t.disabled ? 'is-disabled' : ''}`}
|
||||
activeClassName="is-active"
|
||||
to={currentPath + '/' + t.path}
|
||||
key={i}
|
||||
>
|
||||
{t.name}
|
||||
</NavLink>
|
||||
<SubTabLink tab={t} basePath={basePath} className="SubTabs-tabs-link" key={i} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="SubTabs" ref={el => (this.containerEl = el)}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private handleSelect = ({ value }: Option) => {
|
||||
this.props.history.push(`${this.props.match.url}/${value}`);
|
||||
};
|
||||
|
||||
// Tabs become a dropdown if they would wrap
|
||||
private handleResize = () => {
|
||||
if (!this.containerEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isCollapsed: this.state.tabsWidth >= this.containerEl.offsetWidth
|
||||
});
|
||||
};
|
||||
|
||||
// Store the tab width for future
|
||||
private measureTabsWidth = () => {
|
||||
if (this.tabsEl) {
|
||||
this.setState({ tabsWidth: this.tabsEl.offsetWidth }, () => {
|
||||
this.handleResize();
|
||||
});
|
||||
} else {
|
||||
// Briefly show, measure, collapse again still not enough room
|
||||
this.setState({ isCollapsed: false }, this.measureTabsWidth);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface SubTabLinkProps {
|
||||
tab: Tab;
|
||||
basePath: string;
|
||||
className: string;
|
||||
onClick?(ev: React.MouseEvent<HTMLAnchorElement>): void;
|
||||
}
|
||||
|
||||
const SubTabLink: React.SFC<SubTabLinkProps> = ({ tab, className, basePath, onClick }) => (
|
||||
<NavLink
|
||||
className={`${className} ${tab.disabled ? 'is-disabled' : ''}`}
|
||||
activeClassName="is-active"
|
||||
to={basePath + '/' + tab.path}
|
||||
onClick={onClick}
|
||||
>
|
||||
{tab.name}
|
||||
</NavLink>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import translate from 'translations';
|
||||
import { Identicon, UnitDisplay, NewTabLink, TextArea, Address } from 'components/ui';
|
||||
import { TransactionData, TransactionReceipt } from 'libs/nodes';
|
||||
import { TransactionData, TransactionReceipt } from 'types/transactions';
|
||||
import { NetworkConfig } from 'types/network';
|
||||
import './TransactionDataTable.scss';
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
&-data {
|
||||
text-align: left;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
&-loading {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { Spinner } from 'components/ui';
|
|||
import TransactionDataTable from './TransactionDataTable';
|
||||
import { AppState } from 'reducers';
|
||||
import { NetworkConfig } from 'types/network';
|
||||
import { TransactionState } from 'reducers/transactions';
|
||||
import { TransactionState } from 'types/transactions';
|
||||
import './TransactionStatus.scss';
|
||||
|
||||
interface OwnProps {
|
||||
|
|
|
@ -1,4 +1,17 @@
|
|||
@import 'common/sass/variables';
|
||||
|
||||
.TxHashInput {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
|
||||
&-recent {
|
||||
text-align: left;
|
||||
|
||||
&-separator {
|
||||
display: block;
|
||||
margin: $space-sm 0;
|
||||
text-align: center;
|
||||
color: $gray-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,33 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Select from 'react-select';
|
||||
import moment from 'moment';
|
||||
import translate from 'translations';
|
||||
import { isValidTxHash, isValidETHAddress } from 'libs/validators';
|
||||
import './TxHashInput.scss';
|
||||
import { getRecentNetworkTransactions } from 'selectors/transactions';
|
||||
import { AppState } from 'reducers';
|
||||
import { Input } from 'components/ui';
|
||||
import './TxHashInput.scss';
|
||||
|
||||
interface Props {
|
||||
interface OwnProps {
|
||||
hash?: string;
|
||||
onSubmit(hash: string): void;
|
||||
}
|
||||
interface ReduxProps {
|
||||
recentTxs: AppState['transactions']['recent'];
|
||||
}
|
||||
type Props = OwnProps & ReduxProps;
|
||||
|
||||
interface State {
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export default class TxHashInput extends React.Component<Props, State> {
|
||||
interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
class TxHashInput extends React.Component<Props, State> {
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hash: props.hash || '' };
|
||||
|
@ -26,11 +40,39 @@ export default class TxHashInput extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const { recentTxs } = this.props;
|
||||
const { hash } = this.state;
|
||||
const validClass = hash ? (isValidTxHash(hash) ? '' : 'invalid') : '';
|
||||
const validClass = hash ? (isValidTxHash(hash) ? 'is-valid' : 'is-invalid') : '';
|
||||
let selectOptions: Option[] = [];
|
||||
|
||||
if (recentTxs && recentTxs.length) {
|
||||
selectOptions = recentTxs.map(tx => ({
|
||||
label: `
|
||||
${moment(tx.time).format('lll')}
|
||||
-
|
||||
${tx.from.substr(0, 8)}...
|
||||
to
|
||||
${tx.to.substr(0, 8)}...
|
||||
`,
|
||||
value: tx.hash
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="TxHashInput" onSubmit={this.handleSubmit}>
|
||||
{!!selectOptions.length && (
|
||||
<div className="TxHashInput-recent">
|
||||
<Select
|
||||
value={hash}
|
||||
onChange={this.handleSelectTx}
|
||||
options={selectOptions}
|
||||
placeholder="Select a recent transaction..."
|
||||
searchable={false}
|
||||
/>
|
||||
<em className="TxHashInput-recent-separator">or</em>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Input
|
||||
value={hash}
|
||||
placeholder="0x16e521..."
|
||||
|
@ -55,6 +97,15 @@ export default class TxHashInput extends React.Component<Props, State> {
|
|||
this.setState({ hash: ev.currentTarget.value });
|
||||
};
|
||||
|
||||
private handleSelectTx = (option: Option) => {
|
||||
if (option && option.value) {
|
||||
this.setState({ hash: option.value });
|
||||
this.props.onSubmit(option.value);
|
||||
} else {
|
||||
this.setState({ hash: '' });
|
||||
}
|
||||
};
|
||||
|
||||
private handleSubmit = (ev: React.FormEvent<HTMLFormElement>) => {
|
||||
ev.preventDefault();
|
||||
if (isValidTxHash(this.state.hash)) {
|
||||
|
@ -62,3 +113,7 @@ export default class TxHashInput extends React.Component<Props, State> {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect((state: AppState): ReduxProps => ({
|
||||
recentTxs: getRecentNetworkTransactions(state)
|
||||
}))(TxHashInput);
|
||||
|
|
|
@ -33,6 +33,13 @@ class CheckTransaction extends React.Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
const { network } = this.props;
|
||||
if (network.chainId !== nextProps.network.chainId) {
|
||||
this.setState({ hash: '' });
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { network } = this.props;
|
||||
const { hash } = this.state;
|
||||
|
@ -59,7 +66,7 @@ class CheckTransaction extends React.Component<Props, State> {
|
|||
|
||||
{hash && (
|
||||
<section className="CheckTransaction-tx Tab-content-pane">
|
||||
<TransactionStatusComponent txHash={hash} />
|
||||
<TransactionStatusComponent key={network.chainId} txHash={hash} />
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -28,14 +28,12 @@ const tabs = [
|
|||
|
||||
class Contracts extends Component<Props & RouteComponentProps<{}>> {
|
||||
public render() {
|
||||
const { match } = this.props;
|
||||
const { match, location, history } = this.props;
|
||||
const currentPath = match.url;
|
||||
|
||||
return (
|
||||
<TabSection isUnavailableOffline={true}>
|
||||
<div className="SubTabs-tabs">
|
||||
<SubTabs tabs={tabs} match={match} />
|
||||
</div>
|
||||
<SubTabs tabs={tabs} match={match} location={location} history={history} />
|
||||
<section className="Tab-content Contracts">
|
||||
<div className="Contracts-content">
|
||||
<Switch>
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
@import 'common/sass/variables';
|
||||
@import 'common/sass/mixins';
|
||||
|
||||
$hover-speed: 150ms;
|
||||
$identicon-size: 36px;
|
||||
$identicon-size-mobile: 24px;
|
||||
|
||||
.RecentTx {
|
||||
line-height: $identicon-size;
|
||||
border: 1px solid $gray-lighter;
|
||||
cursor: pointer;
|
||||
transition: box-shadow $hover-speed ease;
|
||||
box-shadow: 0 0 $brand-primary inset;
|
||||
|
||||
&-to {
|
||||
width: 100%;
|
||||
max-width: 0;
|
||||
@include mono;
|
||||
@include ellipsis;
|
||||
|
||||
.Identicon {
|
||||
display: inline-block;
|
||||
width: $identicon-size !important;
|
||||
height: $identicon-size !important;
|
||||
margin-right: $space-md;
|
||||
}
|
||||
}
|
||||
|
||||
&-time {
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
padding-left: $space-md;
|
||||
font-size: 22px;
|
||||
opacity: 0.3;
|
||||
transition-property: opacity, color, transform;
|
||||
transition-duration: $hover-speed;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 3px 0 $brand-primary inset;
|
||||
|
||||
.RecentTx-arrow {
|
||||
opacity: 1;
|
||||
color: $brand-primary;
|
||||
transform: translateX(3px);
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive handling
|
||||
@media (max-width: $screen-md) {
|
||||
font-size: $font-size-xs;
|
||||
line-height: $identicon-size-mobile;
|
||||
|
||||
&-to .Identicon {
|
||||
width: $identicon-size-mobile !important;
|
||||
height: $identicon-size-mobile !important;
|
||||
}
|
||||
|
||||
&-arrow .fa {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { Wei } from 'libs/units';
|
||||
import { Identicon, Address, UnitDisplay } from 'components/ui';
|
||||
import { NetworkConfig } from 'types/network';
|
||||
import { SavedTransaction } from 'types/transactions';
|
||||
import './RecentTransaction.scss';
|
||||
|
||||
interface Props {
|
||||
tx: SavedTransaction;
|
||||
network: NetworkConfig;
|
||||
onClick(hash: string): void;
|
||||
}
|
||||
|
||||
export default class RecentTransaction extends React.Component<Props> {
|
||||
public render() {
|
||||
const { tx, network } = this.props;
|
||||
|
||||
return (
|
||||
<tr className="RecentTx" key={tx.time} onClick={this.handleClick}>
|
||||
<td className="RecentTx-to">
|
||||
<Identicon address={tx.to} />
|
||||
<Address address={tx.to} />
|
||||
</td>
|
||||
<td className="RecentTx-value">
|
||||
<UnitDisplay
|
||||
value={Wei(tx.value)}
|
||||
unit="ether"
|
||||
symbol={network.unit}
|
||||
checkOffline={false}
|
||||
/>
|
||||
</td>
|
||||
<td className="RecentTx-time">{moment(tx.time).format('l LT')}</td>
|
||||
<td className="RecentTx-arrow">
|
||||
<i className="fa fa-chevron-right" />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
private handleClick = () => {
|
||||
this.props.onClick(this.props.tx.hash);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
@import 'common/sass/variables';
|
||||
|
||||
.RecentTxs {
|
||||
position: relative;
|
||||
|
||||
&-txs {
|
||||
width: 100%;
|
||||
|
||||
td {
|
||||
text-align: center;
|
||||
padding: $space-md;
|
||||
white-space: nowrap;
|
||||
|
||||
&:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
thead {
|
||||
font-size: $font-size-bump-more;
|
||||
border-bottom: 2px solid $gray-lighter;
|
||||
|
||||
td {
|
||||
padding-top: $space-xs;
|
||||
padding-bottom: $space-xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-back {
|
||||
display: block;
|
||||
max-width: 300px;
|
||||
margin: $space auto 0;
|
||||
|
||||
.fa {
|
||||
margin-right: $space-xs;
|
||||
opacity: 0.6;
|
||||
transition: $transition;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.fa {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-empty {
|
||||
padding: $space * 3;
|
||||
text-align: center;
|
||||
|
||||
&-text {
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
&-help {
|
||||
max-width: 540px;
|
||||
margin: $space * 2 auto 0;
|
||||
font-size: $font-size-small;
|
||||
text-align: center;
|
||||
color: $gray-light;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import translate from 'translations';
|
||||
import { getRecentWalletTransactions } from 'selectors/transactions';
|
||||
import { getNetworkConfig } from 'selectors/config';
|
||||
import { NewTabLink } from 'components/ui';
|
||||
import RecentTransaction from './RecentTransaction';
|
||||
import { TransactionStatus } from 'components';
|
||||
import { IWallet } from 'libs/wallet';
|
||||
import { NetworkConfig } from 'types/network';
|
||||
import { AppState } from 'reducers';
|
||||
import './RecentTransactions.scss';
|
||||
|
||||
interface OwnProps {
|
||||
wallet: IWallet;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
recentTransactions: AppState['transactions']['recent'];
|
||||
network: NetworkConfig;
|
||||
}
|
||||
|
||||
type Props = OwnProps & StateProps;
|
||||
|
||||
interface State {
|
||||
activeTxHash: string;
|
||||
}
|
||||
|
||||
class RecentTransactions extends React.Component<Props> {
|
||||
public state: State = {
|
||||
activeTxHash: ''
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { activeTxHash } = this.state;
|
||||
let content: React.ReactElement<string>;
|
||||
if (activeTxHash) {
|
||||
content = (
|
||||
<React.Fragment>
|
||||
<TransactionStatus txHash={activeTxHash} />
|
||||
<button className="RecentTxs-back btn btn-default" onClick={this.clearActiveTxHash}>
|
||||
<i className="fa fa-arrow-left" /> Back to Recent Transactions
|
||||
</button>
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
content = this.renderTxList();
|
||||
}
|
||||
|
||||
return <div className="RecentTxs Tab-content-pane">{content}</div>;
|
||||
}
|
||||
|
||||
private renderTxList() {
|
||||
const { wallet, recentTransactions, network } = this.props;
|
||||
|
||||
let explorer: React.ReactElement<string>;
|
||||
if (network.isCustom) {
|
||||
explorer = <span>an explorer for the {network.name} network</span>;
|
||||
} else {
|
||||
explorer = (
|
||||
<NewTabLink href={network.blockExplorer.addressUrl(wallet.getAddressString())}>
|
||||
{network.blockExplorer.name}
|
||||
</NewTabLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{recentTransactions.length ? (
|
||||
<table className="RecentTxs-txs">
|
||||
<thead>
|
||||
<td>{translate('SEND_addr')}</td>
|
||||
<td>{translate('SEND_amount_short')}</td>
|
||||
<td>{translate('Sent')}</td>
|
||||
<td />
|
||||
</thead>
|
||||
<tbody>
|
||||
{recentTransactions.map(tx => (
|
||||
<RecentTransaction
|
||||
key={tx.time}
|
||||
tx={tx}
|
||||
network={network}
|
||||
onClick={this.setActiveTxHash}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className="RecentTxs-empty well">
|
||||
<h2 className="RecentTxs-empty-text">
|
||||
No recent MyCrypto transactions found, try checking on {explorer}.
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
<p className="RecentTxs-help">
|
||||
Only recent transactions sent from this address via MyCrypto on the {network.name} network
|
||||
are listed here. If you don't see your transaction, you can view all of them on {explorer}.
|
||||
</p>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private setActiveTxHash = (activeTxHash: string) => this.setState({ activeTxHash });
|
||||
private clearActiveTxHash = () => this.setState({ activeTxHash: '' });
|
||||
}
|
||||
|
||||
export default connect((state: AppState): StateProps => ({
|
||||
recentTransactions: getRecentWalletTransactions(state),
|
||||
network: getNetworkConfig(state)
|
||||
}))(RecentTransactions);
|
|
@ -4,3 +4,4 @@ export * from './UnavailableWallets';
|
|||
export * from './SideBar';
|
||||
export { default as WalletInfo } from './WalletInfo';
|
||||
export { default as RequestPayment } from './RequestPayment';
|
||||
export { default as RecentTransactions } from './RecentTransactions';
|
||||
|
|
|
@ -3,7 +3,6 @@ import { connect } from 'react-redux';
|
|||
import translate from 'translations';
|
||||
import TabSection from 'containers/TabSection';
|
||||
import { UnlockHeader } from 'components/ui';
|
||||
import { SideBar } from './components/index';
|
||||
import { getWalletInst } from 'selectors/wallet';
|
||||
import { AppState } from 'reducers';
|
||||
import { RouteComponentProps, Route, Switch, Redirect } from 'react-router';
|
||||
|
@ -11,9 +10,11 @@ import { RedirectWithQuery } from 'components/RedirectWithQuery';
|
|||
import {
|
||||
WalletInfo,
|
||||
RequestPayment,
|
||||
RecentTransactions,
|
||||
Fields,
|
||||
UnavailableWallets
|
||||
} from 'containers/Tabs/SendTransaction/components';
|
||||
UnavailableWallets,
|
||||
SideBar
|
||||
} from './components';
|
||||
import SubTabs, { Tab } from 'components/SubTabs';
|
||||
import { RouteNotFound } from 'components/RouteNotFound';
|
||||
import { isNetworkUnit } from 'selectors/config/wallet';
|
||||
|
@ -34,7 +35,7 @@ type Props = StateProps & RouteComponentProps<{}>;
|
|||
|
||||
class SendTransaction extends React.Component<Props> {
|
||||
public render() {
|
||||
const { wallet, match } = this.props;
|
||||
const { wallet, match, location, history } = this.props;
|
||||
const currentPath = match.url;
|
||||
const tabs: Tab[] = [
|
||||
{
|
||||
|
@ -50,6 +51,10 @@ class SendTransaction extends React.Component<Props> {
|
|||
{
|
||||
path: 'info',
|
||||
name: translate('NAV_ViewWallet')
|
||||
},
|
||||
{
|
||||
path: 'recent-txs',
|
||||
name: translate('Recent Transactions')
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -60,7 +65,7 @@ class SendTransaction extends React.Component<Props> {
|
|||
{wallet && (
|
||||
<div className="SubTabs row">
|
||||
<div className="col-sm-8">
|
||||
<SubTabs tabs={tabs} match={match} />
|
||||
<SubTabs tabs={tabs} match={match} location={location} history={history} />
|
||||
</div>
|
||||
<div className="col-sm-8">
|
||||
<Switch>
|
||||
|
@ -91,6 +96,11 @@ class SendTransaction extends React.Component<Props> {
|
|||
exact={true}
|
||||
render={() => <RequestPayment wallet={wallet} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${currentPath}/recent-txs`}
|
||||
exact={true}
|
||||
render={() => <RecentTransactions wallet={wallet} />}
|
||||
/>
|
||||
<RouteNotFound />
|
||||
</Switch>
|
||||
</div>
|
||||
|
|
|
@ -19,7 +19,7 @@ export default class SignAndVerifyMessage extends Component<RouteComponentProps<
|
|||
public changeTab = (activeTab: State['activeTab']) => () => this.setState({ activeTab });
|
||||
|
||||
public render() {
|
||||
const { match } = this.props;
|
||||
const { match, location, history } = this.props;
|
||||
const currentPath = match.url;
|
||||
|
||||
const tabs = [
|
||||
|
@ -36,7 +36,7 @@ export default class SignAndVerifyMessage extends Component<RouteComponentProps<
|
|||
return (
|
||||
<TabSection>
|
||||
<section className="Tab-content SignAndVerifyMsg">
|
||||
<SubTabs tabs={tabs} match={match} />
|
||||
<SubTabs tabs={tabs} match={match} location={location} history={history} />
|
||||
<Switch>
|
||||
<Route
|
||||
exact={true}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Wei, TokenValue } from 'libs/units';
|
||||
import { IHexStrTransaction } from 'libs/transaction';
|
||||
import { Token } from 'types/network';
|
||||
import { TransactionData, TransactionReceipt } from 'types/transactions';
|
||||
|
||||
export interface TxObj {
|
||||
to: string;
|
||||
|
@ -12,33 +13,6 @@ interface TokenBalanceResult {
|
|||
error: string | null;
|
||||
}
|
||||
|
||||
export interface TransactionData {
|
||||
hash: string;
|
||||
nonce: number;
|
||||
blockHash: string | null;
|
||||
blockNumber: number | null;
|
||||
transactionIndex: number | null;
|
||||
from: string;
|
||||
to: string;
|
||||
value: Wei;
|
||||
gasPrice: Wei;
|
||||
gas: Wei;
|
||||
input: string;
|
||||
}
|
||||
|
||||
export interface TransactionReceipt {
|
||||
transactionHash: string;
|
||||
transactionIndex: number;
|
||||
blockHash: string;
|
||||
blockNumber: number;
|
||||
cumulativeGasUsed: Wei;
|
||||
gasUsed: Wei;
|
||||
contractAddress: string | null;
|
||||
logs: string[];
|
||||
logsBloom: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface INode {
|
||||
ping(): Promise<boolean>;
|
||||
getBalance(address: string): Promise<Wei>;
|
||||
|
|
|
@ -2,7 +2,8 @@ import BN from 'bn.js';
|
|||
import { IHexStrTransaction } from 'libs/transaction';
|
||||
import { Wei, TokenValue } from 'libs/units';
|
||||
import { stripHexPrefix } from 'libs/values';
|
||||
import { INode, TxObj, TransactionData, TransactionReceipt } from '../INode';
|
||||
import { hexToNumber } from 'utils/formatters';
|
||||
import { INode, TxObj } from '../INode';
|
||||
import RPCClient from './client';
|
||||
import RPCRequests from './requests';
|
||||
import {
|
||||
|
@ -17,7 +18,7 @@ import {
|
|||
isValidRawTxApi
|
||||
} from 'libs/validators';
|
||||
import { Token } from 'types/network';
|
||||
import { hexToNumber } from 'utils/formatters';
|
||||
import { TransactionData, TransactionReceipt } from 'types/transactions';
|
||||
|
||||
export default class RpcNode implements INode {
|
||||
public client: RPCClient;
|
||||
|
|
|
@ -15,7 +15,6 @@ const computeIndexingHash = (tx: Buffer) => bufferToHex(makeTransaction(tx).hash
|
|||
const getTransactionFields = (t: Tx): IHexStrTransaction => {
|
||||
// For some crazy reason, toJSON spits out an array, not keyed values.
|
||||
const { data, gasLimit, gasPrice, to, nonce, value } = t;
|
||||
|
||||
const chainId = t.getChainId();
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,24 +1,20 @@
|
|||
import {
|
||||
FetchTransactionDataAction,
|
||||
SetTransactionDataAction,
|
||||
AddRecentTransactionAction,
|
||||
TransactionsAction,
|
||||
TypeKeys
|
||||
} from 'actions/transactions';
|
||||
import { TransactionData, TransactionReceipt } from 'libs/nodes';
|
||||
|
||||
export interface TransactionState {
|
||||
data: TransactionData | null;
|
||||
receipt: TransactionReceipt | null;
|
||||
error: string | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
import { SavedTransaction, TransactionState } from 'types/transactions';
|
||||
|
||||
export interface State {
|
||||
txData: { [txhash: string]: TransactionState };
|
||||
recent: SavedTransaction[];
|
||||
}
|
||||
|
||||
export const INITIAL_STATE: State = {
|
||||
txData: {}
|
||||
txData: {},
|
||||
recent: []
|
||||
};
|
||||
|
||||
function fetchTxData(state: State, action: FetchTransactionDataAction): State {
|
||||
|
@ -51,12 +47,30 @@ function setTxData(state: State, action: SetTransactionDataAction): State {
|
|||
};
|
||||
}
|
||||
|
||||
function resetTxData(state: State): State {
|
||||
return {
|
||||
...state,
|
||||
txData: INITIAL_STATE.txData
|
||||
};
|
||||
}
|
||||
|
||||
function addRecentTx(state: State, action: AddRecentTransactionAction): State {
|
||||
return {
|
||||
...state,
|
||||
recent: [action.payload, ...state.recent].slice(0, 50)
|
||||
};
|
||||
}
|
||||
|
||||
export function transactions(state: State = INITIAL_STATE, action: TransactionsAction): State {
|
||||
switch (action.type) {
|
||||
case TypeKeys.TRANSACTIONS_FETCH_TRANSACTION_DATA:
|
||||
return fetchTxData(state, action);
|
||||
case TypeKeys.TRANSACTIONS_SET_TRANSACTION_DATA:
|
||||
return setTxData(state, action);
|
||||
case TypeKeys.TRANSACTIONS_RESET_TRANSACTION_DATA:
|
||||
return resetTxData(state);
|
||||
case TypeKeys.TRANSACTIONS_ADD_RECENT_TRANSACTION:
|
||||
return addRecentTx(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,29 @@
|
|||
import { setTransactionData, FetchTransactionDataAction, TypeKeys } from 'actions/transactions';
|
||||
import { SagaIterator } from 'redux-saga';
|
||||
import { put, select, apply, takeEvery } from 'redux-saga/effects';
|
||||
import { getNodeLib } from 'selectors/config';
|
||||
import { INode, TransactionData, TransactionReceipt } from 'libs/nodes';
|
||||
import { put, select, apply, call, take, takeEvery } from 'redux-saga/effects';
|
||||
import EthTx from 'ethereumjs-tx';
|
||||
import { toChecksumAddress } from 'ethereumjs-util';
|
||||
import {
|
||||
setTransactionData,
|
||||
FetchTransactionDataAction,
|
||||
addRecentTransaction,
|
||||
resetTransactionData,
|
||||
TypeKeys
|
||||
} from 'actions/transactions';
|
||||
import {
|
||||
TypeKeys as TxTypeKeys,
|
||||
BroadcastTransactionQueuedAction,
|
||||
BroadcastTransactionSucceededAction,
|
||||
BroadcastTransactionFailedAction
|
||||
} from 'actions/transaction';
|
||||
import { getNodeLib, getNetworkConfig } from 'selectors/config';
|
||||
import { getWalletInst } from 'selectors/wallet';
|
||||
import { INode } from 'libs/nodes';
|
||||
import { hexEncodeData } from 'libs/nodes/rpc/utils';
|
||||
import { getTransactionFields } from 'libs/transaction';
|
||||
import { TypeKeys as ConfigTypeKeys } from 'actions/config';
|
||||
import { TransactionData, TransactionReceipt, SavedTransaction } from 'types/transactions';
|
||||
import { NetworkConfig } from 'types/network';
|
||||
import { AppState } from 'reducers';
|
||||
|
||||
export function* fetchTxData(action: FetchTransactionDataAction): SagaIterator {
|
||||
const txhash = action.payload;
|
||||
|
@ -34,6 +55,67 @@ export function* fetchTxData(action: FetchTransactionDataAction): SagaIterator {
|
|||
yield put(setTransactionData({ txhash, data, receipt, error }));
|
||||
}
|
||||
|
||||
export function* saveBroadcastedTx(action: BroadcastTransactionQueuedAction) {
|
||||
const { serializedTransaction: txBuffer, indexingHash: txIdx } = action.payload;
|
||||
|
||||
const res: BroadcastTransactionSucceededAction | BroadcastTransactionFailedAction = yield take([
|
||||
TxTypeKeys.BROADCAST_TRANSACTION_SUCCEEDED,
|
||||
TxTypeKeys.BROADCAST_TRASACTION_FAILED
|
||||
]);
|
||||
|
||||
// If our TX succeeded, save it and update the store.
|
||||
if (
|
||||
res.type === TxTypeKeys.BROADCAST_TRANSACTION_SUCCEEDED &&
|
||||
res.payload.indexingHash === txIdx
|
||||
) {
|
||||
const tx = new EthTx(txBuffer);
|
||||
const savableTx: SavedTransaction = yield call(
|
||||
getSaveableTransaction,
|
||||
tx,
|
||||
res.payload.broadcastedHash
|
||||
);
|
||||
yield put(addRecentTransaction(savableTx));
|
||||
}
|
||||
}
|
||||
|
||||
// Given a serialized transaction, return a transaction we could save in LS
|
||||
export function* getSaveableTransaction(tx: EthTx, hash: string): SagaIterator {
|
||||
const fields = getTransactionFields(tx);
|
||||
let from: string = '';
|
||||
let chainId: number = 0;
|
||||
|
||||
try {
|
||||
// Signed transactions have these fields
|
||||
from = hexEncodeData(tx.getSenderAddress());
|
||||
chainId = fields.chainId;
|
||||
} catch (err) {
|
||||
// Unsigned transactions (e.g. web3) don't, so grab them from current state
|
||||
const wallet: AppState['wallet']['inst'] = yield select(getWalletInst);
|
||||
const network: NetworkConfig = yield select(getNetworkConfig);
|
||||
|
||||
chainId = network.chainId;
|
||||
if (wallet) {
|
||||
from = wallet.getAddressString();
|
||||
}
|
||||
}
|
||||
|
||||
const savableTx: SavedTransaction = {
|
||||
hash,
|
||||
from,
|
||||
chainId,
|
||||
to: toChecksumAddress(fields.to),
|
||||
value: fields.value,
|
||||
time: Date.now()
|
||||
};
|
||||
return savableTx;
|
||||
}
|
||||
|
||||
export function* resetTxData() {
|
||||
yield put(resetTransactionData());
|
||||
}
|
||||
|
||||
export default function* transactions(): SagaIterator {
|
||||
yield takeEvery(TypeKeys.TRANSACTIONS_FETCH_TRANSACTION_DATA, fetchTxData);
|
||||
yield takeEvery(TxTypeKeys.BROADCAST_TRANSACTION_QUEUED, saveBroadcastedTx);
|
||||
yield takeEvery(ConfigTypeKeys.CONFIG_NODE_CHANGE, resetTxData);
|
||||
}
|
||||
|
|
|
@ -92,4 +92,12 @@
|
|||
position: relative;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
// Identicons need to fit into the select
|
||||
.Identicon {
|
||||
display: inline-block;
|
||||
width: 22px !important;
|
||||
height: 22px !important;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,30 @@
|
|||
import { AppState } from 'reducers';
|
||||
import { SavedTransaction } from 'types/transactions';
|
||||
import { getNetworkConfig } from './config';
|
||||
import { getWalletInst } from './wallet';
|
||||
|
||||
export function getTransactionDatas(state: AppState) {
|
||||
return state.transactions.txData;
|
||||
}
|
||||
|
||||
export function getRecentTransactions(state: AppState): SavedTransaction[] {
|
||||
return state.transactions.recent;
|
||||
}
|
||||
|
||||
export function getRecentNetworkTransactions(state: AppState): SavedTransaction[] {
|
||||
const txs = getRecentTransactions(state);
|
||||
const network = getNetworkConfig(state);
|
||||
return txs.filter(tx => tx.chainId === network.chainId);
|
||||
}
|
||||
|
||||
export function getRecentWalletTransactions(state: AppState): SavedTransaction[] {
|
||||
const networkTxs = getRecentNetworkTransactions(state);
|
||||
const wallet = getWalletInst(state);
|
||||
|
||||
if (wallet) {
|
||||
const addr = wallet.getAddressString().toLowerCase();
|
||||
return networkTxs.filter(tx => tx.from.toLowerCase() === addr);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,10 @@ import {
|
|||
INITIAL_STATE as transactionInitialState,
|
||||
State as TransactionState
|
||||
} from 'reducers/transaction';
|
||||
import {
|
||||
INITIAL_STATE as initialTransactionsState,
|
||||
State as TransactionsState
|
||||
} from 'reducers/transactions';
|
||||
import { State as SwapState, INITIAL_STATE as swapInitialState } from 'reducers/swap';
|
||||
import { applyMiddleware, createStore, Store } from 'redux';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||
|
@ -44,6 +48,7 @@ const configureStore = () => {
|
|||
: { ...swapInitialState };
|
||||
|
||||
const savedTransactionState = loadStatePropertyOrEmptyObject<TransactionState>('transaction');
|
||||
const savedTransactionsState = loadStatePropertyOrEmptyObject<TransactionsState>('transactions');
|
||||
|
||||
const persistedInitialState: Partial<AppState> = {
|
||||
transaction: {
|
||||
|
@ -62,6 +67,10 @@ const configureStore = () => {
|
|||
|
||||
// ONLY LOAD SWAP STATE FROM LOCAL STORAGE IF STEP WAS 3
|
||||
swap: swapState,
|
||||
transactions: {
|
||||
...initialTransactionsState,
|
||||
...savedTransactionsState
|
||||
},
|
||||
...rehydrateConfigAndCustomTokenState()
|
||||
};
|
||||
|
||||
|
@ -75,6 +84,7 @@ const configureStore = () => {
|
|||
store.subscribe(
|
||||
throttle(() => {
|
||||
const state: AppState = store.getState();
|
||||
|
||||
saveState({
|
||||
transaction: {
|
||||
fields: {
|
||||
|
@ -96,6 +106,9 @@ const configureStore = () => {
|
|||
allIds: []
|
||||
}
|
||||
},
|
||||
transactions: {
|
||||
recent: state.transactions.recent
|
||||
},
|
||||
...getConfigAndCustomTokensStateToSubscribe(state)
|
||||
});
|
||||
}, 50)
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
export const REDUX_STATE = 'REDUX_STATE';
|
||||
import { sha256 } from 'ethereumjs-util';
|
||||
import { State as SwapState } from 'reducers/swap';
|
||||
import { IWallet, WalletConfig } from 'libs/wallet';
|
||||
import { sha256 } from 'ethereumjs-util';
|
||||
import { AppState } from 'reducers';
|
||||
|
||||
export const REDUX_STATE = 'REDUX_STATE';
|
||||
|
||||
export function loadState<T>(): T | undefined {
|
||||
try {
|
||||
const serializedState = localStorage.getItem(REDUX_STATE);
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import { Wei } from 'libs/units';
|
||||
|
||||
export interface SavedTransaction {
|
||||
hash: string;
|
||||
to: string;
|
||||
from: string;
|
||||
value: string;
|
||||
chainId: number;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface TransactionData {
|
||||
hash: string;
|
||||
nonce: number;
|
||||
blockHash: string | null;
|
||||
blockNumber: number | null;
|
||||
transactionIndex: number | null;
|
||||
from: string;
|
||||
to: string;
|
||||
value: Wei;
|
||||
gasPrice: Wei;
|
||||
gas: Wei;
|
||||
input: string;
|
||||
}
|
||||
|
||||
export interface TransactionReceipt {
|
||||
transactionHash: string;
|
||||
transactionIndex: number;
|
||||
blockHash: string;
|
||||
blockNumber: number;
|
||||
cumulativeGasUsed: Wei;
|
||||
gasUsed: Wei;
|
||||
contractAddress: string | null;
|
||||
logs: string[];
|
||||
logsBloom: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface TransactionState {
|
||||
data: TransactionData | null;
|
||||
receipt: TransactionReceipt | null;
|
||||
error: string | null;
|
||||
isLoading: boolean;
|
||||
}
|
Loading…
Reference in New Issue