diff --git a/common/actions/transactions/actionCreators.ts b/common/actions/transactions/actionCreators.ts index 30e2ff95..48c40ef2 100644 --- a/common/actions/transactions/actionCreators.ts +++ b/common/actions/transactions/actionCreators.ts @@ -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 + }; +} diff --git a/common/actions/transactions/actionTypes.ts b/common/actions/transactions/actionTypes.ts index ee4380b3..fd7686b2 100644 --- a/common/actions/transactions/actionTypes.ts +++ b/common/actions/transactions/actionTypes.ts @@ -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; diff --git a/common/actions/transactions/constants.ts b/common/actions/transactions/constants.ts index cbd8ad82..a1834597 100644 --- a/common/actions/transactions/constants.ts +++ b/common/actions/transactions/constants.ts @@ -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' } diff --git a/common/components/SubTabs/SubTabs.scss b/common/components/SubTabs/SubTabs.scss index ea8c667f..e85f7535 100644 --- a/common/components/SubTabs/SubTabs.scss +++ b/common/components/SubTabs/SubTabs.scss @@ -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; + } } diff --git a/common/components/SubTabs/index.tsx b/common/components/SubTabs/index.tsx index 65b92a2f..40ccb23b 100644 --- a/common/components/SubTabs/index.tsx +++ b/common/components/SubTabs/index.tsx @@ -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 { +type Props = OwnProps & RouteComponentProps<{}>; + +interface State { + tabsWidth: number; + isCollapsed: boolean; +} + +export default class SubTabs extends React.PureComponent { + 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; - return ( -
-
+ if (isCollapsed) { + const options = tabs.map(tab => ({ + label: tab.name as string, + value: tab.path, + disabled: tab.disabled + })); + + content = ( +
+ + or +
+ )} + { 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) => { ev.preventDefault(); if (isValidTxHash(this.state.hash)) { @@ -62,3 +113,7 @@ export default class TxHashInput extends React.Component { } }; } + +export default connect((state: AppState): ReduxProps => ({ + recentTxs: getRecentNetworkTransactions(state) +}))(TxHashInput); diff --git a/common/containers/Tabs/CheckTransaction/index.tsx b/common/containers/Tabs/CheckTransaction/index.tsx index 0fae96f0..f9128097 100644 --- a/common/containers/Tabs/CheckTransaction/index.tsx +++ b/common/containers/Tabs/CheckTransaction/index.tsx @@ -33,6 +33,13 @@ class CheckTransaction extends React.Component { } } + 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 { {hash && (
- +
)}
diff --git a/common/containers/Tabs/Contracts/index.tsx b/common/containers/Tabs/Contracts/index.tsx index 77023c8b..6f744386 100644 --- a/common/containers/Tabs/Contracts/index.tsx +++ b/common/containers/Tabs/Contracts/index.tsx @@ -28,14 +28,12 @@ const tabs = [ class Contracts extends Component> { public render() { - const { match } = this.props; + const { match, location, history } = this.props; const currentPath = match.url; return ( -
- -
+
diff --git a/common/containers/Tabs/SendTransaction/components/RecentTransaction.scss b/common/containers/Tabs/SendTransaction/components/RecentTransaction.scss new file mode 100644 index 00000000..88d28937 --- /dev/null +++ b/common/containers/Tabs/SendTransaction/components/RecentTransaction.scss @@ -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; + } + } +} diff --git a/common/containers/Tabs/SendTransaction/components/RecentTransaction.tsx b/common/containers/Tabs/SendTransaction/components/RecentTransaction.tsx new file mode 100644 index 00000000..971bd544 --- /dev/null +++ b/common/containers/Tabs/SendTransaction/components/RecentTransaction.tsx @@ -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 { + public render() { + const { tx, network } = this.props; + + return ( + + + +
+ + + + + {moment(tx.time).format('l LT')} + + + + + ); + } + + private handleClick = () => { + this.props.onClick(this.props.tx.hash); + }; +} diff --git a/common/containers/Tabs/SendTransaction/components/RecentTransactions.scss b/common/containers/Tabs/SendTransaction/components/RecentTransactions.scss new file mode 100644 index 00000000..b2d805a3 --- /dev/null +++ b/common/containers/Tabs/SendTransaction/components/RecentTransactions.scss @@ -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; + } +} diff --git a/common/containers/Tabs/SendTransaction/components/RecentTransactions.tsx b/common/containers/Tabs/SendTransaction/components/RecentTransactions.tsx new file mode 100644 index 00000000..00220d09 --- /dev/null +++ b/common/containers/Tabs/SendTransaction/components/RecentTransactions.tsx @@ -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 { + public state: State = { + activeTxHash: '' + }; + + public render() { + const { activeTxHash } = this.state; + let content: React.ReactElement; + if (activeTxHash) { + content = ( + + + + + ); + } else { + content = this.renderTxList(); + } + + return
{content}
; + } + + private renderTxList() { + const { wallet, recentTransactions, network } = this.props; + + let explorer: React.ReactElement; + if (network.isCustom) { + explorer = an explorer for the {network.name} network; + } else { + explorer = ( + + {network.blockExplorer.name} + + ); + } + + return ( + + {recentTransactions.length ? ( + + + + + + + + {recentTransactions.map(tx => ( + + ))} + +
{translate('SEND_addr')}{translate('SEND_amount_short')}{translate('Sent')} +
+ ) : ( +
+

+ No recent MyCrypto transactions found, try checking on {explorer}. +

+
+ )} +

+ 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}. +

+
+ ); + } + + 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); diff --git a/common/containers/Tabs/SendTransaction/components/index.ts b/common/containers/Tabs/SendTransaction/components/index.ts index a1ca44a8..214d599e 100644 --- a/common/containers/Tabs/SendTransaction/components/index.ts +++ b/common/containers/Tabs/SendTransaction/components/index.ts @@ -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'; diff --git a/common/containers/Tabs/SendTransaction/index.tsx b/common/containers/Tabs/SendTransaction/index.tsx index 521896a1..d5b69521 100644 --- a/common/containers/Tabs/SendTransaction/index.tsx +++ b/common/containers/Tabs/SendTransaction/index.tsx @@ -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 { 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 { { path: 'info', name: translate('NAV_ViewWallet') + }, + { + path: 'recent-txs', + name: translate('Recent Transactions') } ]; @@ -60,7 +65,7 @@ class SendTransaction extends React.Component { {wallet && (
- +
@@ -91,6 +96,11 @@ class SendTransaction extends React.Component { exact={true} render={() => } /> + } + />
diff --git a/common/containers/Tabs/SignAndVerifyMessage/index.tsx b/common/containers/Tabs/SignAndVerifyMessage/index.tsx index 1a35b697..b3930421 100644 --- a/common/containers/Tabs/SignAndVerifyMessage/index.tsx +++ b/common/containers/Tabs/SignAndVerifyMessage/index.tsx @@ -19,7 +19,7 @@ export default class SignAndVerifyMessage extends Component () => 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
- + ; getBalance(address: string): Promise; diff --git a/common/libs/nodes/rpc/index.ts b/common/libs/nodes/rpc/index.ts index bee475aa..8663bfd4 100644 --- a/common/libs/nodes/rpc/index.ts +++ b/common/libs/nodes/rpc/index.ts @@ -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; diff --git a/common/libs/transaction/utils/ether.ts b/common/libs/transaction/utils/ether.ts index 25de3cf0..2cfd73ae 100644 --- a/common/libs/transaction/utils/ether.ts +++ b/common/libs/transaction/utils/ether.ts @@ -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 { diff --git a/common/reducers/transactions.ts b/common/reducers/transactions.ts index b732376c..14a592f4 100644 --- a/common/reducers/transactions.ts +++ b/common/reducers/transactions.ts @@ -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; } diff --git a/common/sagas/transactions.ts b/common/sagas/transactions.ts index 352ecdc1..4c231ca3 100644 --- a/common/sagas/transactions.ts +++ b/common/sagas/transactions.ts @@ -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); } diff --git a/common/sass/styles/overrides/react-select.scss b/common/sass/styles/overrides/react-select.scss index 0fcfbac1..369381c5 100644 --- a/common/sass/styles/overrides/react-select.scss +++ b/common/sass/styles/overrides/react-select.scss @@ -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; + } } diff --git a/common/selectors/transactions.ts b/common/selectors/transactions.ts index b0fc68db..9a262051 100644 --- a/common/selectors/transactions.ts +++ b/common/selectors/transactions.ts @@ -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 []; + } +} diff --git a/common/store/store.ts b/common/store/store.ts index a985d40b..0ef7bd91 100644 --- a/common/store/store.ts +++ b/common/store/store.ts @@ -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('transaction'); + const savedTransactionsState = loadStatePropertyOrEmptyObject('transactions'); const persistedInitialState: Partial = { 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) diff --git a/common/utils/localStorage.ts b/common/utils/localStorage.ts index 05e3d613..5cb8ea24 100644 --- a/common/utils/localStorage.ts +++ b/common/utils/localStorage.ts @@ -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 | undefined { try { const serializedState = localStorage.getItem(REDUX_STATE); diff --git a/shared/types/transactions.d.ts b/shared/types/transactions.d.ts new file mode 100644 index 00000000..681a3f22 --- /dev/null +++ b/shared/types/transactions.d.ts @@ -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; +}