chore: merge conflicts

This commit is contained in:
Andre Neves 2019-02-16 18:47:26 -05:00
commit eec917158e
25 changed files with 410 additions and 2368 deletions

View File

@ -30,6 +30,7 @@ describe('Transactions Actions', () => {
const payload = {
list: [],
zecPrice: 0,
hasNextPage: false,
};
store.dispatch(loadTransactionsSuccess(payload));

View File

@ -14,7 +14,7 @@ describe('<LoadingScreen />', () => {
test('should render status pill correctly', () => {
const { queryByTestId } = render(
<ThemeProvider theme={appTheme}>
<LoadingScreen progress={83.0} />
<LoadingScreen progress={83.0} message='ZEC Wallet Starting' />
</ThemeProvider>,
);

View File

@ -95,6 +95,10 @@ createTestServer({
return res.send({
result: [{ id: 'operation-id-1', status: 'success', result: { txid: 'txid-1' } }],
});
case 'z_getbalance':
return res.send({
result: 5,
});
default:
return null;
}

View File

@ -0,0 +1,13 @@
// @flow
import 'jest-dom/extend-expect';
import { sortByDescend } from '../../app/utils/sort-by-descend';
describe('truncateAddress', () => {
test('should truncate ZEC address', () => {
expect(
sortByDescend('id')([{ id: 5 }, { id: 2 }, { id: 1 }, { id: 0 }, { id: 1 }, { id: 1 }]),
).toEqual([{ id: 5 }, { id: 2 }, { id: 1 }, { id: 1 }, { id: 1 }, { id: 0 }]);
});
});

View File

@ -0,0 +1,13 @@
// @flow
import 'jest-dom/extend-expect';
import { sortBy } from '../../app/utils/sort-by';
describe('truncateAddress', () => {
test('should truncate ZEC address', () => {
expect(
sortBy('id')([{ id: 5 }, { id: 2 }, { id: 1 }, { id: 0 }, { id: 1 }, { id: 1 }]),
).toEqual([{ id: 0 }, { id: 1 }, { id: 1 }, { id: 1 }, { id: 2 }, { id: 5 }]);
});
});

View File

@ -3,8 +3,6 @@
import React, { PureComponent } from 'react';
import styled from 'styled-components';
import { Transition, animated } from 'react-spring';
// eslint-disable-next-line import/no-extraneous-dependencies
import { ipcRenderer } from 'electron';
import CircleProgressComponent from 'react-circle';
import { TextComponent } from './text';
@ -41,35 +39,27 @@ const Logo = styled.img`
type Props = {
progress: number,
message: string,
};
type State = {
start: boolean,
message: string,
};
const TIME_DELAY_ANIM = 100;
export class LoadingScreen extends PureComponent<Props, State> {
state = { start: false, message: 'ZEC Wallet Starting' };
state = { start: false };
componentDidMount() {
setTimeout(() => {
this.setState(() => ({ start: true }));
}, TIME_DELAY_ANIM);
ipcRenderer.on('zcashd-params-download', (event: Object, message: string) => {
this.setState(() => ({ message }));
});
}
componentWillUnmount() {
ipcRenderer.removeAllListeners('zcashd-log');
}
render() {
const { start, message } = this.state;
const { progress } = this.props;
const { start } = this.state;
const { progress, message } = this.props;
return (
<Wrapper data-testid='LoadingScreen'>

View File

@ -38,6 +38,7 @@ const StyledLink = styled.a`
width: 100%;
display: flex;
align-items: center;
outline: none;
cursor: pointer;
outline: none;
transition: all 0.03s ${(props: StyledLinkProps) => props.theme.transitionEase};

View File

@ -188,7 +188,13 @@ export const TransactionDetailsComponent = ({
<InfoRow>
<ColumnComponent width='100%'>
<Label value='TRANSACTION ID' />
<TransactionId onClick={() => openExternal(ZCASH_EXPLORER_BASE_URL + transactionId)}>
<TransactionId
onClick={
from !== '(Shielded)'
? () => openExternal(ZCASH_EXPLORER_BASE_URL + transactionId)
: () => {}
}
>
<Ellipsis value={transactionId} />
</TransactionId>
</ColumnComponent>

View File

@ -14,6 +14,7 @@ const AddressWrapper = styled.div`
background-color: #000;
border-radius: 6px;
padding: 7px 13px;
margin-bottom: 10px;
width: 100%;
`;
@ -38,7 +39,7 @@ const QRCodeWrapper = styled.div`
background-color: ${props => props.theme.colors.qrCodeWrapperBg}
border-radius: 6px;
padding: 20px;
margin-top: 10px;
margin-bottom: 10px;
width: 100%;
`;

View File

@ -11,6 +11,7 @@ type Props = {};
type State = {
isRunning: boolean,
progress: number,
message: string,
};
/* eslint-disable max-len */
@ -22,6 +23,7 @@ export const withDaemonStatusCheck = <PassedProps: {}>(
state = {
isRunning: false,
progress: 0,
message: 'ZEC Wallet Starting',
};
componentDidMount() {
@ -53,21 +55,23 @@ export const withDaemonStatusCheck = <PassedProps: {}>(
}
}
})
.catch(() => {
.catch((error) => {
const statusMessage = error.message === 'Something went wrong' ? 'ZEC Wallet Starting' : error.message;
this.setState((state) => {
const newProgress = state.progress > 70 ? state.progress + 2.5 : state.progress + 5;
return { progress: newProgress > 95 ? 95 : newProgress };
return { progress: newProgress > 95 ? 95 : newProgress, message: statusMessage };
});
});
};
render() {
const { isRunning, progress } = this.state;
const { isRunning, progress, message } = this.state;
if (isRunning) {
return <WrappedComponent {...this.props} {...this.state} />;
}
return <LoadingScreen progress={progress} />;
return <LoadingScreen progress={progress} message={message} />;
}
};

View File

@ -8,13 +8,14 @@ import dateFns from 'date-fns';
import { BigNumber } from 'bignumber.js';
import { DashboardView } from '../views/dashboard';
import rpc from '../../services/api';
import { listShieldedTransactions } from '../../services/shielded-transactions';
import store from '../../config/electron-store';
import {
loadWalletSummary,
loadWalletSummarySuccess,
loadWalletSummaryError,
} from '../redux/modules/wallet';
import { sortBy } from '../utils/sort-by';
import { sortByDescend } from '../utils/sort-by-descend';
import type { AppState } from '../types/app-state';
import type { Dispatch } from '../types/redux';
@ -47,7 +48,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
);
}
const formattedTransactions = flow([
const formattedTransactions: Array<Object> = flow([
arr => arr.map(transaction => ({
transactionId: transaction.txid,
type: transaction.category,
@ -58,10 +59,11 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
arr => groupBy(arr, obj => dateFns.format(obj.date, 'MMM DD, YYYY')),
obj => Object.keys(obj).map(day => ({
day,
list: sortBy('date')(obj[day]),
jsDay: new Date(day),
list: sortByDescend('date')(obj[day]),
})),
sortBy('day'),
])(transactions);
sortByDescend('jsDay'),
])([...transactions, ...listShieldedTransactions()]);
if (!zAddresses.length) {
const [, newZAddress] = await eres(rpc.z_getnewaddress());

View File

@ -21,6 +21,7 @@ import {
} from '../redux/modules/send';
import { filterObjectNullKeys } from '../utils/filter-object-null-keys';
import { saveShieldedTransaction } from '../../services/shielded-transactions';
import type { AppState } from '../types/app-state';
import type { Dispatch } from '../types/redux';
@ -92,6 +93,15 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
if (operationStatus && operationStatus.status === 'success') {
clearInterval(interval);
if (from.startsWith('z')) {
saveShieldedTransaction({
category: 'send',
time: Date.now() / 1000,
address: '(Shielded)',
amount: new BigNumber(amount).toNumber(),
memo,
});
}
dispatch(sendTransactionSuccess({ operationId: operationStatus.result.txid }));
}

View File

@ -2,62 +2,82 @@
import eres from 'eres';
import { connect } from 'react-redux';
import flow from 'lodash.flow';
import groupBy from 'lodash.groupby';
import dateFns from 'date-fns';
import { BigNumber } from 'bignumber.js';
import uuidv4 from 'uuid/v4';
import { TransactionsView } from '../views/transactions';
import {
loadTransactions,
loadTransactionsSuccess,
loadTransactionsError,
resetTransactionsList,
} from '../redux/modules/transactions';
import rpc from '../../services/api';
import { listShieldedTransactions } from '../../services/shielded-transactions';
import store from '../../config/electron-store';
import { sortBy } from '../utils/sort-by';
import { sortByDescend } from '../utils/sort-by-descend';
import type { AppState } from '../types/app-state';
import type { Dispatch } from '../types/redux';
import type { Transaction } from '../components/transaction-item';
const mapStateToProps = ({ transactions }: AppState) => ({
transactions: transactions.list,
isLoading: transactions.isLoading,
error: transactions.error,
zecPrice: transactions.zecPrice,
hasNextPage: transactions.hasNextPage,
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
getTransactions: async () => {
export type MapStateToProps = {
transactions: Transaction[],
isLoading: boolean,
error: string | null,
zecPrice: number,
hasNextPage: boolean,
};
export type MapDispatchToProps = {|
getTransactions: ({
offset: number,
count: number,
shieldedTransactionsCount: number,
}) => Promise<void>,
resetTransactionsList: () => void,
|};
const mapDispatchToProps = (dispatch: Dispatch): MapDispatchToProps => ({
resetTransactionsList: () => dispatch(resetTransactionsList()),
getTransactions: async ({ offset, count, shieldedTransactionsCount }) => {
dispatch(loadTransactions());
const [transactionsErr, transactions = []] = await eres(rpc.listtransactions());
const [transactionsErr, transactions = []] = await eres(
rpc.listtransactions('', count, offset),
);
if (transactionsErr) {
return dispatch(loadTransactionsError({ error: transactionsErr.message }));
}
const formattedTransactions = flow([
arr => arr.map(transaction => ({
transactionId: transaction.txid,
const formattedTransactions = sortByDescend('date')(
[
...transactions,
...listShieldedTransactions({ count, offset: shieldedTransactionsCount }),
].map(transaction => ({
transactionId: transaction.txid ? transaction.txid : uuidv4(),
type: transaction.category,
date: new Date(transaction.time * 1000).toISOString(),
address: transaction.address,
amount: new BigNumber(transaction.amount).absoluteValue().toNumber(),
})),
arr => groupBy(arr, obj => dateFns.format(obj.date, 'MMM DD, YYYY')),
obj => Object.keys(obj).map(day => ({
day,
list: sortBy('date')(obj[day]),
})),
sortBy('day'),
])(transactions);
);
dispatch(
loadTransactionsSuccess({
list: formattedTransactions,
zecPrice: new BigNumber(store.get('ZEC_DOLLAR_PRICE')).toNumber(),
hasNextPage: Boolean(formattedTransactions.length),
}),
);
},

View File

@ -1,5 +1,5 @@
// @flow
import uniqBy from 'lodash.uniqby';
import type { Action } from '../../types/redux';
import type { Transaction } from '../../components/transaction-item';
@ -7,6 +7,7 @@ import type { Transaction } from '../../components/transaction-item';
export const LOAD_TRANSACTIONS = 'LOAD_TRANSACTIONS';
export const LOAD_TRANSACTIONS_SUCCESS = 'LOAD_TRANSACTIONS_SUCCESS';
export const LOAD_TRANSACTIONS_ERROR = 'LOAD_TRANSACTIONS_ERROR';
export const RESET_TRANSACTIONS_LIST = 'RESET_TRANSACTIONS_LIST';
export type TransactionsList = { day: string, list: Transaction[] }[];
@ -18,14 +19,17 @@ export const loadTransactions = () => ({
export const loadTransactionsSuccess = ({
list,
zecPrice,
hasNextPage,
}: {
list: TransactionsList,
list: Transaction[],
zecPrice: number,
hasNextPage: boolean,
}) => ({
type: LOAD_TRANSACTIONS_SUCCESS,
payload: {
list,
zecPrice,
hasNextPage,
},
});
@ -34,11 +38,17 @@ export const loadTransactionsError = ({ error }: { error: string }) => ({
payload: { error },
});
export const resetTransactionsList = () => ({
type: RESET_TRANSACTIONS_LIST,
payload: {},
});
export type State = {
isLoading: boolean,
error: string | null,
list: TransactionsList,
list: Transaction[],
zecPrice: number,
hasNextPage: boolean,
};
const initialState = {
@ -46,6 +56,7 @@ const initialState = {
list: [],
error: null,
isLoading: false,
hasNextPage: true,
};
// eslint-disable-next-line
@ -61,6 +72,7 @@ export default (state: State = initialState, action: Action) => {
return {
...state,
...action.payload,
list: uniqBy(state.list.concat(action.payload.list), tr => tr.transactionId + tr.type),
isLoading: false,
error: null,
};
@ -70,6 +82,13 @@ export default (state: State = initialState, action: Action) => {
isLoading: false,
error: action.payload.error,
};
case RESET_TRANSACTIONS_LIST:
return {
...state,
isLoading: false,
error: null,
list: [],
};
default:
return state;
}

View File

@ -0,0 +1,5 @@
// @flow
/* eslint-disable max-len */
// $FlowFixMe
export const sortByDescend = <T>(field: string) => (arr: T[]): T[] => arr.sort((a, b) => (a[field] < b[field] ? 1 : -1));

View File

@ -1,5 +1,5 @@
// @flow
/* eslint-disable max-len */
// $FlowFixMe
export const sortBy = <T>(field: string) => (arr: T[]): T[] => arr.sort((a, b) => (a[field] < b[field] ? 1 : -1));
export const sortBy = <T>(field: string) => (arr: T[]): T[] => arr.sort((a, b) => (a[field] > b[field] ? 1 : -1));

View File

@ -1,6 +1,4 @@
// @flow
export const truncateAddress = (address: string = '') => `${address.substr(0, 20)}...${address.substr(
address.length - 10,
address.length,
)}`;
export const truncateAddress = (address: string = '') => (address.length < 20
? address
: `${address.substr(0, 20)}...${address.substr(address.length - 10, address.length)}`);

View File

@ -159,11 +159,9 @@ const ConfirmItemWrapper = styled(RowComponent)`
width: 100%;
`;
type ItemLabelProps =
| {
color: string,
}
| Object;
type ItemLabelProps = {
color: string,
};
/* eslint-disable max-len */
const ItemLabel = styled(TextComponent)`
font-weight: ${(props: PropsWithTheme<ItemLabelProps>) => String(props.theme.fontWeight.bold)};
@ -227,7 +225,7 @@ const MaxAvailableAmount = styled.button`
background: none;
color: white;
cursor: pointer;
border-left: ${props => `1px solid ${props.theme.colors.background}`};
border-left: ${props => `1px solid ${props.theme.colors.background(props)}`};
opacity: 0.8;
&:hover {

View File

@ -1,48 +1,198 @@
// @flow
import React, { PureComponent, Fragment } from 'react';
import React, { PureComponent, Fragment, type Element } from 'react';
import { InfiniteLoader, AutoSizer, List } from 'react-virtualized';
import styled from 'styled-components';
import dateFns from 'date-fns';
import { TransactionDailyComponent } from '../components/transaction-daily';
import { TransactionItemComponent } from '../components/transaction-item';
import { TextComponent } from '../components/text';
import { EmptyTransactionsComponent } from '../components/empty-transactions';
import type { TransactionsList } from '../redux/modules/transactions';
import type { MapDispatchToProps, MapStateToProps } from '../containers/transactions';
type Props = {
error: string | null,
transactions: TransactionsList,
zecPrice: number,
getTransactions: () => void,
};
type Props = MapDispatchToProps & MapStateToProps;
const PAGE_SIZE = 15;
const ROW_HEIGHT = 60;
const ROW_HEIGHT_WITH_HEADER = 88;
const Day = styled(TextComponent)`
text-transform: uppercase;
color: ${props => props.theme.colors.transactionsDate};
font-size: ${props => `${props.theme.fontSize.regular * 0.9}em`};
font-weight: ${props => String(props.theme.fontWeight.bold)};
margin-top: 10px;
margin-bottom: 5px;
`;
const RoundedTransactionWrapper = styled.div`
overflow: hidden;
${props => (props.roundPosition === 'top'
? `
border-top-left-radius: ${props.theme.boxBorderRadius};
border-top-right-radius: ${props.theme.boxBorderRadius};`
: `border-bottom-left-radius: ${props.theme.boxBorderRadius};
border-bottom-right-radius: ${props.theme.boxBorderRadius};`)}
`;
export class TransactionsView extends PureComponent<Props> {
componentDidMount() {
// eslint-disable-next-line
this.props.getTransactions();
const { getTransactions, resetTransactionsList } = this.props;
resetTransactionsList();
getTransactions({ count: PAGE_SIZE, offset: 0, shieldedTransactionsCount: 0 });
}
isRowLoaded = ({ index }: { index: number }) => {
const { hasNextPage, transactions } = this.props;
const transactionsSize = transactions.length;
return !hasNextPage || index < transactionsSize;
};
renderTransactionWrapper = ({
index,
transactionDate,
previousTransactionDate,
nextTransactionDate,
component,
}: {|
index: number,
transactionDate: Date,
previousTransactionDate: ?Date,
nextTransactionDate: ?Date,
component: Element<*>,
|}) => {
if (
index === 0
|| (previousTransactionDate && !dateFns.isSameDay(transactionDate, previousTransactionDate))
) {
return <RoundedTransactionWrapper roundPosition='top'>{component}</RoundedTransactionWrapper>;
}
if (
nextTransactionDate
&& (nextTransactionDate && !dateFns.isSameDay(transactionDate, nextTransactionDate))
) {
return (
<RoundedTransactionWrapper roundPosition='bottom'>{component}</RoundedTransactionWrapper>
);
}
return component;
};
renderTransactions = ({ index }: { index: number }) => {
const { transactions, zecPrice } = this.props;
const transaction = transactions[index];
const previousTransaction = transactions[index - 1];
const nextTransaction = transactions[index + 1];
const transactionItem = this.renderTransactionWrapper({
transactionDate: new Date(transaction.date),
previousTransactionDate: previousTransaction ? new Date(previousTransaction.date) : null,
nextTransactionDate: nextTransaction ? new Date(nextTransaction.date) : null,
component: (
<TransactionItemComponent
address={transaction.address}
amount={transaction.amount}
date={transaction.date}
transactionId={transaction.transactionId}
type={transaction.type}
zecPrice={zecPrice}
/>
),
index,
});
if (
index === 0
|| (previousTransaction
&& !dateFns.isSameDay(new Date(previousTransaction.date), new Date(transaction.date)))
) {
return (
<Fragment>
<Day value={dateFns.format(new Date(transaction.date), 'MMM DD, YYYY')} />
{transactionItem}
</Fragment>
);
}
return transactionItem;
};
renderRow = ({ index, key, style }: { index: number, key: string, style: Object }) => (
<div key={key} style={style}>
{this.isRowLoaded({ index }) ? this.renderTransactions({ index }) : 'Loading...'}
</div>
);
getRowHeight = ({ index }: { index: number }) => {
const { transactions } = this.props;
const transaction = transactions[index];
if (
index === 0
|| !dateFns.isSameDay(new Date(transactions[index - 1].date), new Date(transaction.date))
) {
return ROW_HEIGHT_WITH_HEADER;
}
return ROW_HEIGHT;
};
loadNextPage = () => {
const { transactions, getTransactions } = this.props;
const shieldedTransactionsCount = transactions.filter(
transaction => transaction.address === '(Shielded)',
).length;
getTransactions({ count: PAGE_SIZE, offset: transactions.length, shieldedTransactionsCount });
};
loadMoreRows = async () => {
const { isLoading } = this.props;
return isLoading ? Promise.resolve([]) : this.loadNextPage();
};
render() {
const { error, transactions, zecPrice } = this.props;
const { error, transactions, hasNextPage } = this.props;
const transactionsSize = transactions.length;
const isRowLoaded = ({ index }) => !hasNextPage || index < transactionsSize;
const rowCount = transactionsSize ? transactionsSize + 1 : transactionsSize;
if (error) {
return <TextComponent value={error} />;
}
return (
<Fragment>
{transactions.length === 0 ? (
<EmptyTransactionsComponent />
) : (
transactions.map(({ day, list }) => (
<TransactionDailyComponent
transactionsDate={day}
transactions={list}
zecPrice={zecPrice}
key={day}
/>
))
<InfiniteLoader
isRowLoaded={isRowLoaded}
loadMoreRows={this.loadMoreRows}
rowCount={rowCount}
>
{({ onRowsRendered, registerChild }) => (
<AutoSizer>
{({ width, height }) => (
<List
noRowsRenderer={EmptyTransactionsComponent}
ref={registerChild}
onRowsRendered={onRowsRendered}
rowRenderer={this.renderRow}
rowHeight={this.getRowHeight}
rowCount={transactionsSize}
width={width}
height={height - 20}
/>
)}
</AutoSizer>
)}
</Fragment>
</InfiniteLoader>
);
}
}

View File

@ -2,6 +2,17 @@
set -eu
# Create default zcash.conf
if [[ "$OSTYPE" == "darwin"* ]]; then
if [ ! -e "$HOME/Library/Application Support/Zcash/zcash.conf" ] ; then
echo "server=1" > "$HOME/Library/Application Support/Zcash/zcash.conf"
fi
else
if [ ! -e "$HOME/.zcash/zcash.conf" ] ; then
echo "server=1" > "$HOME/.zcash/zcash.conf"
fi
fi
if [[ "$OSTYPE" == "darwin"* ]]; then
PARAMS_DIR="$HOME/Library/Application Support/ZcashParams"
else

View File

@ -10,9 +10,9 @@ declare module 'electron-store' {
fileExtension?: string,
}): ElectronStore;
set(key: string, value: string): void;
set(key: string, value: any): void;
set(payload: Object): void;
get(key: string): string;
get(key: string): any;
has(key: string): boolean;
delete(key: string): void;
clear(): void;

View File

@ -0,0 +1,3 @@
declare module 'lodash.uniqby' {
declare module.exports: <T>(arr: T[], (T) => $Values<T>) => T[];
}

View File

@ -108,6 +108,7 @@
"history": "^4.7.2",
"lodash.flow": "^3.5.0",
"lodash.groupby": "^4.6.0",
"lodash.uniqby": "^4.7.0",
"p-queue": "^3.0.0",
"process-exists": "^3.1.0",
"qrcode.react": "^0.8.0",
@ -119,6 +120,7 @@
"react-redux": "^5.0.7",
"react-router-dom": "^4.2.2",
"react-spring": "^7.2.10",
"react-virtualized": "^9.21.0",
"redux": "^4.0.1",
"redux-thunk": "^2.2.0",
"styled-components": "^4.1.1",

View File

@ -0,0 +1,47 @@
// @flow
import electronStore from '../config/electron-store';
const STORE_KEY = 'SHIELDED_TRANSACTIONS';
type ShieldedTransaction = {|
category: 'send' | 'receive',
time: number,
address: string,
amount: number,
memo: ?string,
|};
// eslint-disable-next-line
export const listShieldedTransactions = (
pagination: ?{
offset: number,
count: number,
},
): Array<ShieldedTransaction> => {
const transactions = electronStore.has(STORE_KEY) ? electronStore.get(STORE_KEY) : [];
if (!pagination) return transactions;
const { offset = 0, count = 10 } = pagination;
return transactions.slice(offset - 1, offset + count);
};
export const saveShieldedTransaction = ({
category,
time,
address,
amount,
memo,
}: ShieldedTransaction): void => {
electronStore.set(
STORE_KEY,
listShieldedTransactions().concat({
category,
time,
address,
amount,
memo: memo || '',
}),
);
};

2308
yarn.lock

File diff suppressed because it is too large Load Diff