mirror of
https://github.com/status-im/MyCrypto.git
synced 2025-01-09 18:45:38 +00:00
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
|
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 { TypeKeys } from './constants';
|
||||||
import { TransactionData, TransactionReceipt } from 'libs/nodes';
|
import { SavedTransaction, TransactionData, TransactionReceipt } from 'types/transactions';
|
||||||
|
|
||||||
export interface FetchTransactionDataAction {
|
export interface FetchTransactionDataAction {
|
||||||
type: TypeKeys.TRANSACTIONS_FETCH_TRANSACTION_DATA;
|
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 ***/
|
/*** Union Type ***/
|
||||||
export type TransactionsAction = FetchTransactionDataAction | SetTransactionDataAction;
|
export type TransactionsAction =
|
||||||
|
| FetchTransactionDataAction
|
||||||
|
| SetTransactionDataAction
|
||||||
|
| ResetTransactionDataAction
|
||||||
|
| AddRecentTransactionAction;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
export enum TypeKeys {
|
export enum TypeKeys {
|
||||||
TRANSACTIONS_FETCH_TRANSACTION_DATA = 'TRANSACTIONS_FETCH_TRANSACTION_DATA',
|
TRANSACTIONS_FETCH_TRANSACTION_DATA = 'TRANSACTIONS_FETCH_TRANSACTION_DATA',
|
||||||
TRANSACTIONS_SET_TRANSACTION_DATA = 'TRANSACTIONS_SET_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;
|
margin-top: 15px;
|
||||||
|
|
||||||
&-tabs {
|
&-tabs {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&-link {
|
&-link {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@ -26,4 +29,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-select {
|
||||||
|
margin-bottom: $space-md;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Select, { Option } from 'react-select';
|
||||||
import { NavLink, RouteComponentProps } from 'react-router-dom';
|
import { NavLink, RouteComponentProps } from 'react-router-dom';
|
||||||
import './SubTabs.scss';
|
import './SubTabs.scss';
|
||||||
|
|
||||||
@ -9,32 +10,135 @@ export interface Tab {
|
|||||||
redirect?: string;
|
redirect?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface OwnProps {
|
||||||
tabs: Tab[];
|
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() {
|
public render() {
|
||||||
const { tabs, match } = this.props;
|
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 (
|
if (isCollapsed) {
|
||||||
<div className="SubTabs row">
|
const options = tabs.map(tab => ({
|
||||||
<div className="SubTabs-tabs col-sm-12">
|
label: tab.name as string,
|
||||||
{tabs.map((t, i) => (
|
value: tab.path,
|
||||||
// Same as normal Link, but knows when it's active, and applies activeClassName
|
disabled: tab.disabled
|
||||||
<NavLink
|
}));
|
||||||
className={`SubTabs-tabs-link ${t.disabled ? 'is-disabled' : ''}`}
|
|
||||||
activeClassName="is-active"
|
content = (
|
||||||
to={currentPath + '/' + t.path}
|
<div className="SubTabs-select">
|
||||||
key={i}
|
<Select
|
||||||
>
|
options={options}
|
||||||
{t.name}
|
value={currentPath.split('/').pop()}
|
||||||
</NavLink>
|
onChange={this.handleSelect}
|
||||||
))}
|
searchable={false}
|
||||||
|
clearable={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// All tabs visible navigation
|
||||||
|
content = (
|
||||||
|
<div className="SubTabs-tabs" ref={el => (this.tabsEl = el)}>
|
||||||
|
{tabs.map((t, i) => (
|
||||||
|
<SubTabLink tab={t} basePath={basePath} className="SubTabs-tabs-link" key={i} />
|
||||||
|
))}
|
||||||
</div>
|
</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 React from 'react';
|
||||||
import translate from 'translations';
|
import translate from 'translations';
|
||||||
import { Identicon, UnitDisplay, NewTabLink, TextArea, Address } from 'components/ui';
|
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 { NetworkConfig } from 'types/network';
|
||||||
import './TransactionDataTable.scss';
|
import './TransactionDataTable.scss';
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
&-data {
|
&-data {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-loading {
|
&-loading {
|
||||||
|
@ -8,7 +8,7 @@ import { Spinner } from 'components/ui';
|
|||||||
import TransactionDataTable from './TransactionDataTable';
|
import TransactionDataTable from './TransactionDataTable';
|
||||||
import { AppState } from 'reducers';
|
import { AppState } from 'reducers';
|
||||||
import { NetworkConfig } from 'types/network';
|
import { NetworkConfig } from 'types/network';
|
||||||
import { TransactionState } from 'reducers/transactions';
|
import { TransactionState } from 'types/transactions';
|
||||||
import './TransactionStatus.scss';
|
import './TransactionStatus.scss';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
|
@ -1,4 +1,17 @@
|
|||||||
|
@import 'common/sass/variables';
|
||||||
|
|
||||||
.TxHashInput {
|
.TxHashInput {
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
margin: 0 auto;
|
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 React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Select from 'react-select';
|
||||||
|
import moment from 'moment';
|
||||||
import translate from 'translations';
|
import translate from 'translations';
|
||||||
import { isValidTxHash, isValidETHAddress } from 'libs/validators';
|
import { isValidTxHash, isValidETHAddress } from 'libs/validators';
|
||||||
import './TxHashInput.scss';
|
import { getRecentNetworkTransactions } from 'selectors/transactions';
|
||||||
|
import { AppState } from 'reducers';
|
||||||
import { Input } from 'components/ui';
|
import { Input } from 'components/ui';
|
||||||
|
import './TxHashInput.scss';
|
||||||
|
|
||||||
interface Props {
|
interface OwnProps {
|
||||||
hash?: string;
|
hash?: string;
|
||||||
onSubmit(hash: string): void;
|
onSubmit(hash: string): void;
|
||||||
}
|
}
|
||||||
|
interface ReduxProps {
|
||||||
|
recentTxs: AppState['transactions']['recent'];
|
||||||
|
}
|
||||||
|
type Props = OwnProps & ReduxProps;
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
hash: string;
|
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) {
|
public constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { hash: props.hash || '' };
|
this.state = { hash: props.hash || '' };
|
||||||
@ -26,11 +40,39 @@ export default class TxHashInput extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
const { recentTxs } = this.props;
|
||||||
const { hash } = this.state;
|
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 (
|
return (
|
||||||
<form className="TxHashInput" onSubmit={this.handleSubmit}>
|
<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
|
<Input
|
||||||
value={hash}
|
value={hash}
|
||||||
placeholder="0x16e521..."
|
placeholder="0x16e521..."
|
||||||
@ -55,6 +97,15 @@ export default class TxHashInput extends React.Component<Props, State> {
|
|||||||
this.setState({ hash: ev.currentTarget.value });
|
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>) => {
|
private handleSubmit = (ev: React.FormEvent<HTMLFormElement>) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (isValidTxHash(this.state.hash)) {
|
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() {
|
public render() {
|
||||||
const { network } = this.props;
|
const { network } = this.props;
|
||||||
const { hash } = this.state;
|
const { hash } = this.state;
|
||||||
@ -59,7 +66,7 @@ class CheckTransaction extends React.Component<Props, State> {
|
|||||||
|
|
||||||
{hash && (
|
{hash && (
|
||||||
<section className="CheckTransaction-tx Tab-content-pane">
|
<section className="CheckTransaction-tx Tab-content-pane">
|
||||||
<TransactionStatusComponent txHash={hash} />
|
<TransactionStatusComponent key={network.chainId} txHash={hash} />
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,14 +28,12 @@ const tabs = [
|
|||||||
|
|
||||||
class Contracts extends Component<Props & RouteComponentProps<{}>> {
|
class Contracts extends Component<Props & RouteComponentProps<{}>> {
|
||||||
public render() {
|
public render() {
|
||||||
const { match } = this.props;
|
const { match, location, history } = this.props;
|
||||||
const currentPath = match.url;
|
const currentPath = match.url;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabSection isUnavailableOffline={true}>
|
<TabSection isUnavailableOffline={true}>
|
||||||
<div className="SubTabs-tabs">
|
<SubTabs tabs={tabs} match={match} location={location} history={history} />
|
||||||
<SubTabs tabs={tabs} match={match} />
|
|
||||||
</div>
|
|
||||||
<section className="Tab-content Contracts">
|
<section className="Tab-content Contracts">
|
||||||
<div className="Contracts-content">
|
<div className="Contracts-content">
|
||||||
<Switch>
|
<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 * from './SideBar';
|
||||||
export { default as WalletInfo } from './WalletInfo';
|
export { default as WalletInfo } from './WalletInfo';
|
||||||
export { default as RequestPayment } from './RequestPayment';
|
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 translate from 'translations';
|
||||||
import TabSection from 'containers/TabSection';
|
import TabSection from 'containers/TabSection';
|
||||||
import { UnlockHeader } from 'components/ui';
|
import { UnlockHeader } from 'components/ui';
|
||||||
import { SideBar } from './components/index';
|
|
||||||
import { getWalletInst } from 'selectors/wallet';
|
import { getWalletInst } from 'selectors/wallet';
|
||||||
import { AppState } from 'reducers';
|
import { AppState } from 'reducers';
|
||||||
import { RouteComponentProps, Route, Switch, Redirect } from 'react-router';
|
import { RouteComponentProps, Route, Switch, Redirect } from 'react-router';
|
||||||
@ -11,9 +10,11 @@ import { RedirectWithQuery } from 'components/RedirectWithQuery';
|
|||||||
import {
|
import {
|
||||||
WalletInfo,
|
WalletInfo,
|
||||||
RequestPayment,
|
RequestPayment,
|
||||||
|
RecentTransactions,
|
||||||
Fields,
|
Fields,
|
||||||
UnavailableWallets
|
UnavailableWallets,
|
||||||
} from 'containers/Tabs/SendTransaction/components';
|
SideBar
|
||||||
|
} from './components';
|
||||||
import SubTabs, { Tab } from 'components/SubTabs';
|
import SubTabs, { Tab } from 'components/SubTabs';
|
||||||
import { RouteNotFound } from 'components/RouteNotFound';
|
import { RouteNotFound } from 'components/RouteNotFound';
|
||||||
import { isNetworkUnit } from 'selectors/config/wallet';
|
import { isNetworkUnit } from 'selectors/config/wallet';
|
||||||
@ -34,7 +35,7 @@ type Props = StateProps & RouteComponentProps<{}>;
|
|||||||
|
|
||||||
class SendTransaction extends React.Component<Props> {
|
class SendTransaction extends React.Component<Props> {
|
||||||
public render() {
|
public render() {
|
||||||
const { wallet, match } = this.props;
|
const { wallet, match, location, history } = this.props;
|
||||||
const currentPath = match.url;
|
const currentPath = match.url;
|
||||||
const tabs: Tab[] = [
|
const tabs: Tab[] = [
|
||||||
{
|
{
|
||||||
@ -50,6 +51,10 @@ class SendTransaction extends React.Component<Props> {
|
|||||||
{
|
{
|
||||||
path: 'info',
|
path: 'info',
|
||||||
name: translate('NAV_ViewWallet')
|
name: translate('NAV_ViewWallet')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'recent-txs',
|
||||||
|
name: translate('Recent Transactions')
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -60,7 +65,7 @@ class SendTransaction extends React.Component<Props> {
|
|||||||
{wallet && (
|
{wallet && (
|
||||||
<div className="SubTabs row">
|
<div className="SubTabs row">
|
||||||
<div className="col-sm-8">
|
<div className="col-sm-8">
|
||||||
<SubTabs tabs={tabs} match={match} />
|
<SubTabs tabs={tabs} match={match} location={location} history={history} />
|
||||||
</div>
|
</div>
|
||||||
<div className="col-sm-8">
|
<div className="col-sm-8">
|
||||||
<Switch>
|
<Switch>
|
||||||
@ -91,6 +96,11 @@ class SendTransaction extends React.Component<Props> {
|
|||||||
exact={true}
|
exact={true}
|
||||||
render={() => <RequestPayment wallet={wallet} />}
|
render={() => <RequestPayment wallet={wallet} />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path={`${currentPath}/recent-txs`}
|
||||||
|
exact={true}
|
||||||
|
render={() => <RecentTransactions wallet={wallet} />}
|
||||||
|
/>
|
||||||
<RouteNotFound />
|
<RouteNotFound />
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,7 +19,7 @@ export default class SignAndVerifyMessage extends Component<RouteComponentProps<
|
|||||||
public changeTab = (activeTab: State['activeTab']) => () => this.setState({ activeTab });
|
public changeTab = (activeTab: State['activeTab']) => () => this.setState({ activeTab });
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { match } = this.props;
|
const { match, location, history } = this.props;
|
||||||
const currentPath = match.url;
|
const currentPath = match.url;
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
@ -36,7 +36,7 @@ export default class SignAndVerifyMessage extends Component<RouteComponentProps<
|
|||||||
return (
|
return (
|
||||||
<TabSection>
|
<TabSection>
|
||||||
<section className="Tab-content SignAndVerifyMsg">
|
<section className="Tab-content SignAndVerifyMsg">
|
||||||
<SubTabs tabs={tabs} match={match} />
|
<SubTabs tabs={tabs} match={match} location={location} history={history} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
exact={true}
|
exact={true}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Wei, TokenValue } from 'libs/units';
|
import { Wei, TokenValue } from 'libs/units';
|
||||||
import { IHexStrTransaction } from 'libs/transaction';
|
import { IHexStrTransaction } from 'libs/transaction';
|
||||||
import { Token } from 'types/network';
|
import { Token } from 'types/network';
|
||||||
|
import { TransactionData, TransactionReceipt } from 'types/transactions';
|
||||||
|
|
||||||
export interface TxObj {
|
export interface TxObj {
|
||||||
to: string;
|
to: string;
|
||||||
@ -12,33 +13,6 @@ interface TokenBalanceResult {
|
|||||||
error: string | null;
|
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 {
|
export interface INode {
|
||||||
ping(): Promise<boolean>;
|
ping(): Promise<boolean>;
|
||||||
getBalance(address: string): Promise<Wei>;
|
getBalance(address: string): Promise<Wei>;
|
||||||
|
@ -2,7 +2,8 @@ import BN from 'bn.js';
|
|||||||
import { IHexStrTransaction } from 'libs/transaction';
|
import { IHexStrTransaction } from 'libs/transaction';
|
||||||
import { Wei, TokenValue } from 'libs/units';
|
import { Wei, TokenValue } from 'libs/units';
|
||||||
import { stripHexPrefix } from 'libs/values';
|
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 RPCClient from './client';
|
||||||
import RPCRequests from './requests';
|
import RPCRequests from './requests';
|
||||||
import {
|
import {
|
||||||
@ -17,7 +18,7 @@ import {
|
|||||||
isValidRawTxApi
|
isValidRawTxApi
|
||||||
} from 'libs/validators';
|
} from 'libs/validators';
|
||||||
import { Token } from 'types/network';
|
import { Token } from 'types/network';
|
||||||
import { hexToNumber } from 'utils/formatters';
|
import { TransactionData, TransactionReceipt } from 'types/transactions';
|
||||||
|
|
||||||
export default class RpcNode implements INode {
|
export default class RpcNode implements INode {
|
||||||
public client: RPCClient;
|
public client: RPCClient;
|
||||||
|
@ -15,7 +15,6 @@ const computeIndexingHash = (tx: Buffer) => bufferToHex(makeTransaction(tx).hash
|
|||||||
const getTransactionFields = (t: Tx): IHexStrTransaction => {
|
const getTransactionFields = (t: Tx): IHexStrTransaction => {
|
||||||
// For some crazy reason, toJSON spits out an array, not keyed values.
|
// For some crazy reason, toJSON spits out an array, not keyed values.
|
||||||
const { data, gasLimit, gasPrice, to, nonce, value } = t;
|
const { data, gasLimit, gasPrice, to, nonce, value } = t;
|
||||||
|
|
||||||
const chainId = t.getChainId();
|
const chainId = t.getChainId();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,24 +1,20 @@
|
|||||||
import {
|
import {
|
||||||
FetchTransactionDataAction,
|
FetchTransactionDataAction,
|
||||||
SetTransactionDataAction,
|
SetTransactionDataAction,
|
||||||
|
AddRecentTransactionAction,
|
||||||
TransactionsAction,
|
TransactionsAction,
|
||||||
TypeKeys
|
TypeKeys
|
||||||
} from 'actions/transactions';
|
} from 'actions/transactions';
|
||||||
import { TransactionData, TransactionReceipt } from 'libs/nodes';
|
import { SavedTransaction, TransactionState } from 'types/transactions';
|
||||||
|
|
||||||
export interface TransactionState {
|
|
||||||
data: TransactionData | null;
|
|
||||||
receipt: TransactionReceipt | null;
|
|
||||||
error: string | null;
|
|
||||||
isLoading: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
txData: { [txhash: string]: TransactionState };
|
txData: { [txhash: string]: TransactionState };
|
||||||
|
recent: SavedTransaction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const INITIAL_STATE: State = {
|
export const INITIAL_STATE: State = {
|
||||||
txData: {}
|
txData: {},
|
||||||
|
recent: []
|
||||||
};
|
};
|
||||||
|
|
||||||
function fetchTxData(state: State, action: FetchTransactionDataAction): State {
|
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 {
|
export function transactions(state: State = INITIAL_STATE, action: TransactionsAction): State {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case TypeKeys.TRANSACTIONS_FETCH_TRANSACTION_DATA:
|
case TypeKeys.TRANSACTIONS_FETCH_TRANSACTION_DATA:
|
||||||
return fetchTxData(state, action);
|
return fetchTxData(state, action);
|
||||||
case TypeKeys.TRANSACTIONS_SET_TRANSACTION_DATA:
|
case TypeKeys.TRANSACTIONS_SET_TRANSACTION_DATA:
|
||||||
return setTxData(state, action);
|
return setTxData(state, action);
|
||||||
|
case TypeKeys.TRANSACTIONS_RESET_TRANSACTION_DATA:
|
||||||
|
return resetTxData(state);
|
||||||
|
case TypeKeys.TRANSACTIONS_ADD_RECENT_TRANSACTION:
|
||||||
|
return addRecentTx(state, action);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,29 @@
|
|||||||
import { setTransactionData, FetchTransactionDataAction, TypeKeys } from 'actions/transactions';
|
|
||||||
import { SagaIterator } from 'redux-saga';
|
import { SagaIterator } from 'redux-saga';
|
||||||
import { put, select, apply, takeEvery } from 'redux-saga/effects';
|
import { put, select, apply, call, take, takeEvery } from 'redux-saga/effects';
|
||||||
import { getNodeLib } from 'selectors/config';
|
import EthTx from 'ethereumjs-tx';
|
||||||
import { INode, TransactionData, TransactionReceipt } from 'libs/nodes';
|
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 {
|
export function* fetchTxData(action: FetchTransactionDataAction): SagaIterator {
|
||||||
const txhash = action.payload;
|
const txhash = action.payload;
|
||||||
@ -34,6 +55,67 @@ export function* fetchTxData(action: FetchTransactionDataAction): SagaIterator {
|
|||||||
yield put(setTransactionData({ txhash, data, receipt, error }));
|
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 {
|
export default function* transactions(): SagaIterator {
|
||||||
yield takeEvery(TypeKeys.TRANSACTIONS_FETCH_TRANSACTION_DATA, fetchTxData);
|
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;
|
position: relative;
|
||||||
height: inherit;
|
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 { AppState } from 'reducers';
|
||||||
|
import { SavedTransaction } from 'types/transactions';
|
||||||
|
import { getNetworkConfig } from './config';
|
||||||
|
import { getWalletInst } from './wallet';
|
||||||
|
|
||||||
export function getTransactionDatas(state: AppState) {
|
export function getTransactionDatas(state: AppState) {
|
||||||
return state.transactions.txData;
|
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,
|
INITIAL_STATE as transactionInitialState,
|
||||||
State as TransactionState
|
State as TransactionState
|
||||||
} from 'reducers/transaction';
|
} 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 { State as SwapState, INITIAL_STATE as swapInitialState } from 'reducers/swap';
|
||||||
import { applyMiddleware, createStore, Store } from 'redux';
|
import { applyMiddleware, createStore, Store } from 'redux';
|
||||||
import { composeWithDevTools } from 'redux-devtools-extension';
|
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||||
@ -44,6 +48,7 @@ const configureStore = () => {
|
|||||||
: { ...swapInitialState };
|
: { ...swapInitialState };
|
||||||
|
|
||||||
const savedTransactionState = loadStatePropertyOrEmptyObject<TransactionState>('transaction');
|
const savedTransactionState = loadStatePropertyOrEmptyObject<TransactionState>('transaction');
|
||||||
|
const savedTransactionsState = loadStatePropertyOrEmptyObject<TransactionsState>('transactions');
|
||||||
|
|
||||||
const persistedInitialState: Partial<AppState> = {
|
const persistedInitialState: Partial<AppState> = {
|
||||||
transaction: {
|
transaction: {
|
||||||
@ -62,6 +67,10 @@ const configureStore = () => {
|
|||||||
|
|
||||||
// ONLY LOAD SWAP STATE FROM LOCAL STORAGE IF STEP WAS 3
|
// ONLY LOAD SWAP STATE FROM LOCAL STORAGE IF STEP WAS 3
|
||||||
swap: swapState,
|
swap: swapState,
|
||||||
|
transactions: {
|
||||||
|
...initialTransactionsState,
|
||||||
|
...savedTransactionsState
|
||||||
|
},
|
||||||
...rehydrateConfigAndCustomTokenState()
|
...rehydrateConfigAndCustomTokenState()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -75,6 +84,7 @@ const configureStore = () => {
|
|||||||
store.subscribe(
|
store.subscribe(
|
||||||
throttle(() => {
|
throttle(() => {
|
||||||
const state: AppState = store.getState();
|
const state: AppState = store.getState();
|
||||||
|
|
||||||
saveState({
|
saveState({
|
||||||
transaction: {
|
transaction: {
|
||||||
fields: {
|
fields: {
|
||||||
@ -96,6 +106,9 @@ const configureStore = () => {
|
|||||||
allIds: []
|
allIds: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
transactions: {
|
||||||
|
recent: state.transactions.recent
|
||||||
|
},
|
||||||
...getConfigAndCustomTokensStateToSubscribe(state)
|
...getConfigAndCustomTokensStateToSubscribe(state)
|
||||||
});
|
});
|
||||||
}, 50)
|
}, 50)
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
export const REDUX_STATE = 'REDUX_STATE';
|
import { sha256 } from 'ethereumjs-util';
|
||||||
import { State as SwapState } from 'reducers/swap';
|
import { State as SwapState } from 'reducers/swap';
|
||||||
import { IWallet, WalletConfig } from 'libs/wallet';
|
import { IWallet, WalletConfig } from 'libs/wallet';
|
||||||
import { sha256 } from 'ethereumjs-util';
|
|
||||||
import { AppState } from 'reducers';
|
import { AppState } from 'reducers';
|
||||||
|
|
||||||
|
export const REDUX_STATE = 'REDUX_STATE';
|
||||||
|
|
||||||
export function loadState<T>(): T | undefined {
|
export function loadState<T>(): T | undefined {
|
||||||
try {
|
try {
|
||||||
const serializedState = localStorage.getItem(REDUX_STATE);
|
const serializedState = localStorage.getItem(REDUX_STATE);
|
||||||
|
44
shared/types/transactions.d.ts
vendored
Normal file
44
shared/types/transactions.d.ts
vendored
Normal file
@ -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…
x
Reference in New Issue
Block a user