Merge pull request #16 from andrerfneves/feature/recent-transactions

WIP: Feature/recent transactions
This commit is contained in:
George Lima 2018-12-15 19:06:06 -02:00 committed by GitHub
commit 22de395e3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 638 additions and 87 deletions

View File

@ -34,7 +34,7 @@
"max-len": [
"error",
{
"code": 120,
"code": 80,
"tabWidth": 2,
"ignoreUrls": true,
"ignoreComments": true,

View File

@ -32,6 +32,8 @@ describe('WalletSummary Actions', () => {
transparent: 5000,
shielded: 5000,
addresses: [],
transactions: {},
zecPrice: 50,
};
store.dispatch(loadWalletSummarySuccess(payload));

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.342 18.853"><defs><style>.a{fill:#6aeac0;}</style></defs><g transform="translate(34.55 55.562) rotate(180)"><g transform="translate(22.05 41.044)"><path class="a" d="M2.822,2.39h.817l3.889-.011c.69,0,1.381,0,2.071,0a.836.836,0,0,1,.1,1.668,1.591,1.591,0,0,1-.187.007q-4.277,0-8.55,0a1.1,1.1,0,0,1-.511-.093A.811.811,0,0,1,.228,2.67Q1.426,1.444,2.642.233A.8.8,0,0,1,3.777.24a.8.8,0,0,1,.015,1.127c-.295.31-.6.612-.907.918a.4.4,0,0,1-.1.056A.407.407,0,0,1,2.822,2.39Z" transform="translate(6.505 0) rotate(90)"/><path class="a" d="M2.822,1.664h.817l3.889.011c.69,0,1.381,0,2.071,0A.836.836,0,0,0,9.7.011,1.591,1.591,0,0,0,9.517,0Q5.24,0,.967,0A1.1,1.1,0,0,0,.455.093.811.811,0,0,0,.228,1.385q1.2,1.226,2.415,2.437A.808.808,0,0,0,3.792,2.687c-.295-.31-.6-.612-.907-.918a.4.4,0,0,0-.1-.056A.407.407,0,0,0,2.822,1.664Z" transform="translate(4.054 0) rotate(90)"/></g><g transform="translate(16.208 36.709)"><g transform="translate(0 0)"><path class="a" d="M9.568,0c.192.018.384.033.572.054a8.642,8.642,0,0,1,3.97,1.44,9.218,9.218,0,0,1,4.013,5.978,9.494,9.494,0,0,1-.923,6.481.923.923,0,0,0-.047.1.812.812,0,0,1-.919.5.847.847,0,0,1-.666-.85.325.325,0,0,1,.029-.13c.159-.38.322-.76.478-1.143a7.694,7.694,0,0,0,.59-3.456,7.687,7.687,0,0,0-2.4-5.236,7.093,7.093,0,0,0-4.071-1.979A7.2,7.2,0,0,0,4.249,3.579a7.447,7.447,0,0,0-2.5,4.614,7.65,7.65,0,0,0,1.925,6.492,7.156,7.156,0,0,0,4.429,2.377,7.222,7.222,0,0,0,5.453-1.39.387.387,0,0,0,.058-.051.8.8,0,0,1,.9-.17.871.871,0,0,1,.5.8.711.711,0,0,1-.275.572,8.448,8.448,0,0,1-4.48,1.954,8.725,8.725,0,0,1-6.846-2A9.249,9.249,0,0,1,.182,11.337a9.4,9.4,0,0,1-.127-3A9.343,9.343,0,0,1,2.36,3.108,8.962,8.962,0,0,1,7.234.214a8.521,8.521,0,0,1,1.419-.2A1.044,1.044,0,0,0,8.758,0C9.029,0,9.3,0,9.568,0Z" transform="translate(0.01)"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.342 18.853"><defs><style>.a{fill:#ff6c6c;}</style></defs><g transform="translate(-16.208 -36.709)"><g transform="translate(22.05 41.044)"><path class="a" d="M2.822,2.39h.817l3.889-.011c.69,0,1.381,0,2.071,0a.836.836,0,0,1,.1,1.668,1.591,1.591,0,0,1-.187.007q-4.277,0-8.55,0a1.1,1.1,0,0,1-.511-.093A.811.811,0,0,1,.228,2.67Q1.426,1.444,2.642.233A.8.8,0,0,1,3.777.24a.8.8,0,0,1,.015,1.127c-.295.31-.6.612-.907.918a.4.4,0,0,1-.1.056A.407.407,0,0,1,2.822,2.39Z" transform="translate(6.505 0) rotate(90)"/><path class="a" d="M2.822,1.664h.817l3.889.011c.69,0,1.381,0,2.071,0A.836.836,0,0,0,9.7.011,1.591,1.591,0,0,0,9.517,0Q5.24,0,.967,0A1.1,1.1,0,0,0,.455.093.811.811,0,0,0,.228,1.385q1.2,1.226,2.415,2.437A.808.808,0,0,0,3.792,2.687c-.295-.31-.6-.612-.907-.918a.4.4,0,0,0-.1-.056A.407.407,0,0,0,2.822,1.664Z" transform="translate(4.054 0) rotate(90)"/></g><g transform="translate(16.208 36.709)"><g transform="translate(0 0)"><path class="a" d="M9.568,0c.192.018.384.033.572.054a8.642,8.642,0,0,1,3.97,1.44,9.218,9.218,0,0,1,4.013,5.978,9.494,9.494,0,0,1-.923,6.481.923.923,0,0,0-.047.1.812.812,0,0,1-.919.5.847.847,0,0,1-.666-.85.325.325,0,0,1,.029-.13c.159-.38.322-.76.478-1.143a7.694,7.694,0,0,0,.59-3.456,7.687,7.687,0,0,0-2.4-5.236,7.093,7.093,0,0,0-4.071-1.979A7.2,7.2,0,0,0,4.249,3.579a7.447,7.447,0,0,0-2.5,4.614,7.65,7.65,0,0,0,1.925,6.492,7.156,7.156,0,0,0,4.429,2.377,7.222,7.222,0,0,0,5.453-1.39.387.387,0,0,0,.058-.051.8.8,0,0,1,.9-.17.871.871,0,0,1,.5.8.711.711,0,0,1-.275.572,8.448,8.448,0,0,1-4.48,1.954,8.725,8.725,0,0,1-6.846-2A9.249,9.249,0,0,1,.182,11.337a9.4,9.4,0,0,1-.127-3A9.343,9.343,0,0,1,2.36,3.108,8.962,8.962,0,0,1,7.234.214a8.521,8.521,0,0,1,1.419-.2A1.044,1.044,0,0,0,8.758,0C9.029,0,9.3,0,9.568,0Z" transform="translate(0.01)"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -4,6 +4,7 @@ import React from 'react';
import styled from 'styled-components';
import { Link } from 'react-router-dom';
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable max-len */
// $FlowFixMe
import { darken } from 'polished';

29
app/components/column.js Normal file
View File

@ -0,0 +1,29 @@
// @flow
import React from 'react';
import styled from 'styled-components';
import type { Node } from 'react';
const Flex = styled.div`
display: flex;
flex-direction: column;
align-items: ${props => props.alignItems};
justify-content: ${props => props.justifyContent};
`;
type Props = {
alignItems?: string,
justifyContent?: string,
className?: string,
children: Node,
};
export const ColumnComponent = ({ children, ...props }: Props) => (
<Flex {...props}>{React.Children.map(children, ch => ch)}</Flex>
);
ColumnComponent.defaultProps = {
alignItems: 'flex-start',
justifyContent: 'flex-start',
className: '',
};

View File

@ -11,7 +11,10 @@ import { TextComponent } from './text';
/* eslint-disable max-len */
const MenuWrapper = styled.div`
background-image: ${props => `linear-gradient(to right, ${darken(0.05, props.theme.colors.activeItem)}, ${props.theme.colors.activeItem})`};
background-image: ${props => `linear-gradient(to right, ${darken(
0.05,
props.theme.colors.activeItem,
)}, ${props.theme.colors.activeItem})`};
padding: 10px 20px;
border-radius: 10px;
margin-left: -10px;
@ -77,7 +80,9 @@ export class DropdownComponent extends Component<Props, State> {
preferPlace='below'
enterExitTransitionDurationMs={0}
body={[
<ClickOutside onClickOutside={() => this.setState(() => ({ isOpen: false }))}>
<ClickOutside
onClickOutside={() => this.setState(() => ({ isOpen: false }))}
>
<MenuWrapper>
{label && (
<MenuItem disabled>
@ -94,7 +99,10 @@ export class DropdownComponent extends Component<Props, State> {
]}
tipSize={7}
>
{renderTrigger(() => this.setState(state => ({ isOpen: !state.isOpen })), isOpen)}
{renderTrigger(
() => this.setState(state => ({ isOpen: !state.isOpen })),
isOpen,
)}
</PopoverWithStyle>
);
}

View File

@ -36,8 +36,12 @@ type Props = {
export const InputComponent = ({ inputType, onChange, ...props }: Props) => {
const inputTypes = {
input: () => <Input onChange={evt => onChange(evt.target.value)} {...props} />,
textarea: () => <Textarea onChange={evt => onChange(evt.target.value)} {...props} />,
input: () => (
<Input onChange={evt => onChange(evt.target.value)} {...props} />
),
textarea: () => (
<Textarea onChange={evt => onChange(evt.target.value)} {...props} />
),
dropdown: () => null,
};

View File

@ -8,7 +8,9 @@ type Props = {
size?: number,
};
export const QRCode = ({ value, size }: Props) => <QR value={value} size={size} />;
export const QRCode = ({ value, size }: Props) => (
<QR value={value} size={size} />
);
QRCode.defaultProps = {
size: 128,

View File

@ -14,6 +14,7 @@ const Flex = styled.div`
type Props = {
alignItems?: string,
justifyContent?: string,
className?: string,
children: Node,
};
@ -24,4 +25,5 @@ export const RowComponent = ({ children, ...props }: Props) => (
RowComponent.defaultProps = {
alignItems: 'flex-start',
justifyContent: 'flex-start',
className: '',
};

View File

@ -16,10 +16,14 @@ const Wrapper = styled.div`
`;
const StyledLink = styled(Link)`
color: ${props => (props.isActive ? props.theme.colors.sidebarItemActive : props.theme.colors.sidebarItem)};
color: ${props => (props.isActive
? props.theme.colors.sidebarItemActive
: props.theme.colors.sidebarItem)};
font-size: ${props => `${props.theme.fontSize.text}em`};
text-decoration: none;
font-weight: ${props => (props.isActive ? props.theme.fontWeight.bold : props.theme.fontWeight.default)};
font-weight: ${props => (props.isActive
? props.theme.fontWeight.bold
: props.theme.fontWeight.default)};
padding: 0 20px;
height: 35px;
width: 100%;
@ -27,11 +31,15 @@ const StyledLink = styled(Link)`
display: flex;
align-items: center;
outline: none;
border-right: ${props => (props.isActive ? `1px solid ${props.theme.colors.sidebarItemActive}` : 'none')};
border-right: ${props => (props.isActive
? `1px solid ${props.theme.colors.sidebarItemActive}`
: 'none')};
&:hover {
color: ${/* eslint-disable-next-line max-len */
props => (props.isActive ? props.theme.colors.sidebarItemActive : props.theme.colors.sidebarHoveredItemLabel)};
props => (props.isActive
? props.theme.colors.sidebarItemActive
: props.theme.colors.sidebarHoveredItemLabel)};
background-color: ${props => props.theme.colors.sidebarHoveredItem};
}
`;

View File

@ -10,7 +10,10 @@ const Text = styled.p`
color: ${props => props.color || props.theme.colors.text};
margin: 0;
padding: 0;
font-weight: ${props => (props.isBold ? props.theme.fontWeight.bold : props.theme.fontWeight.default)};
font-weight: ${props => (props.isBold
? props.theme.fontWeight.bold
: props.theme.fontWeight.default)};
text-align: ${props => props.align};
`;
type Props = {
@ -19,12 +22,24 @@ type Props = {
color?: string,
className?: string,
size?: string | number,
align?: string,
};
export const TextComponent = ({
value, isBold, color, className, size,
value,
isBold,
color,
className,
size,
align,
}: Props) => (
<Text className={className} isBold={isBold} color={color} size={`${String(size)}em`}>
<Text
className={className}
isBold={isBold}
color={color}
size={`${String(size)}em`}
align={align}
>
{value}
</Text>
);
@ -33,5 +48,6 @@ TextComponent.defaultProps = {
className: '',
isBold: false,
color: theme.colors.text,
size: `${theme.fontSize.text}em`,
size: theme.fontSize.text,
align: 'left',
};

View File

@ -0,0 +1,65 @@
// @flow
import React from 'react';
import styled from 'styled-components';
import { TransactionItemComponent, type Transaction } from './transaction-item';
import { TextComponent } from './text';
const Wrapper = styled.div`
margin-top: 20px;
`;
const TransactionsWrapper = styled.div`
border-radius: 7.5px;
overflow: hidden;
background-color: ${props => props.theme.colors.cardBackgroundColor};
padding: 0;
margin: 0;
box-sizing: border-box;
margin-bottom: 20px;
`;
const Day = styled(TextComponent)`
text-transform: uppercase;
color: ${props => props.theme.colors.transactionsDate};
font-size: ${props => `${props.theme.fontSize.text * 0.9}em`};
font-weight: ${props => props.theme.fontWeight.bold};
margin-bottom: 5px;
`;
const Divider = styled.div`
width: 100%;
height: 1px;
background-color: ${props => props.theme.colors.inactiveItem};
`;
type Props = {
transactionsDate: string,
transactions: Transaction[],
zecPrice: number,
};
export const TransactionDailyComponent = ({
transactionsDate,
transactions,
zecPrice,
}: Props) => (
<Wrapper>
<Day value={transactionsDate} />
<TransactionsWrapper>
{transactions.map(({
date, type, address, amount,
}, idx) => (
<div>
<TransactionItemComponent
type={type}
date={date}
address={address || ''}
amount={amount}
zecPrice={zecPrice}
/>
{idx < transactions.length - 1 && <Divider />}
</div>
))}
</TransactionsWrapper>
</Wrapper>
);

View File

@ -0,0 +1,38 @@
---
name: Transaction Daily
---
import { Playground, PropsTable } from 'docz'
import { TransactionDailyComponent } from './transaction-daily.js'
import { DoczWrapper } from '../theme.js'
# Transaction Item
<PropsTable of={TransactionDailyComponent} />
## Basic Usage
<Playground>
<DoczWrapper>
{() => (
<TransactionDailyComponent
transactionsDate={new Date().toISOString()}
transactions={[
{
type: 'received',
address: '123456789123456789123456789123456789',
amount: 1.7891,
date: new Date().toISOString(),
},
{
type: 'sent',
address: '123456789123456789123456789123456789',
amount: 0.8458,
date: new Date().toISOString(),
},
]}
/>
)}
</DoczWrapper>
</Playground>

View File

@ -0,0 +1,103 @@
// @flow
import React from 'react';
import styled from 'styled-components';
import dateFns from 'date-fns';
import SentIcon from '../assets/images/transaction_sent_icon.svg';
import ReceivedIcon from '../assets/images/transaction_received_icon.svg';
import { RowComponent } from './row';
import { ColumnComponent } from './column';
import { TextComponent } from './text';
import theme from '../theme';
import formatNumber from '../utils/formatNumber';
import truncateAddress from '../utils/truncateAddress';
const Wrapper = styled(RowComponent)`
background-color: ${props => props.theme.colors.cardBackgroundColor};
padding: 15px 17px;
`;
const Icon = styled.img`
width: 20px;
height: 20px;
`;
const TransactionTypeLabel = styled(TextComponent)`
color: ${props => (props.isReceived
? props.theme.colors.transactionReceived
: props.theme.colors.transactionSent)};
text-transform: capitalize;
`;
const TransactionTime = styled(TextComponent)`
color: ${props => props.theme.colors.inactiveItem};
`;
const TransactionColumn = styled(ColumnComponent)`
margin-left: 10px;
margin-right: 80px;
min-width: 60px;
`;
export type Transaction = {
type: 'send' | 'receive',
date: string,
address: string,
amount: number,
zecPrice: number,
};
export const TransactionItemComponent = ({
type,
date,
address,
amount,
zecPrice,
}: Transaction) => {
const isReceived = type === 'receive';
const transactionTime = dateFns.format(new Date(date), 'HH:mm A');
const transactionValueInZec = formatNumber({
value: amount,
append: `${isReceived ? '+' : '-'}ZEC `,
});
const transactionValueInUsd = formatNumber({
value: amount * zecPrice,
append: `${isReceived ? '+' : '-'}USD $`,
});
const transactionAddress = truncateAddress(address);
return (
<Wrapper alignItems='center' justifyContent='space-between'>
<RowComponent alignItems='center'>
<RowComponent alignItems='center'>
<Icon
src={isReceived ? ReceivedIcon : SentIcon}
alt='Transaction Type Icon'
/>
<TransactionColumn>
<TransactionTypeLabel isReceived={isReceived} value={type} />
<TransactionTime value={transactionTime} />
</TransactionColumn>
</RowComponent>
<TextComponent value={transactionAddress} align='left' />
</RowComponent>
<ColumnComponent alignItems='flex-end'>
<TextComponent
value={transactionValueInZec}
color={
isReceived
? theme.colors.transactionReceived
: theme.colors.transactionSent
}
/>
<TextComponent
value={transactionValueInUsd}
color={theme.colors.inactiveItem}
/>
</ColumnComponent>
</Wrapper>
);
};

View File

@ -0,0 +1,42 @@
---
name: Transaction Item
---
import { Playground, PropsTable } from 'docz'
import { TransactionItemComponent } from './transaction-item.js'
import { DoczWrapper } from '../theme.js'
# Transaction Item
<PropsTable of={TransactionItemComponent} />
## Sent
<Playground>
<DoczWrapper>
{() => (
<TransactionItemComponent
type="sent"
address="123456789123456789123456789123456789"
amount={0.8652}
date={new Date().toISOString()}
/>
)}
</DoczWrapper>
</Playground>
## Received
<Playground>
<DoczWrapper>
{() => (
<TransactionItemComponent
type="received"
address="123456789123456789123456789123456789"
amount={1.7891}
date={new Date().toISOString()}
/>
)}
</DoczWrapper>
</Playground>

View File

@ -7,6 +7,8 @@ import { RowComponent } from './row';
import { DropdownComponent } from './dropdown';
import MenuIcon from '../assets/images/menu_icon.svg';
import formatNumber from '../utils/formatNumber';
import theme from '../theme';
const Wrapper = styled.div`
@ -51,7 +53,9 @@ const SeeMoreButton = styled.button`
border-style: solid;
border-radius: 100%;
border-width: 1px;
border-color: ${props => (props.isOpen ? props.theme.colors.activeItem : props.theme.colors.inactiveItem)};
border-color: ${props => (props.isOpen
? props.theme.colors.activeItem
: props.theme.colors.inactiveItem)};
background-color: transparent;
padding: 5px;
cursor: pointer;
@ -73,14 +77,16 @@ type Props = {
total: number,
shielded: number,
transparent: number,
dollarValue: number,
zecPrice: number,
addresses: string[],
};
const formatNumber = number => number.toLocaleString('de-DE');
export const WalletSummaryComponent = ({
total, shielded, transparent, dollarValue, addresses,
total,
shielded,
transparent,
zecPrice,
addresses,
}: Props) => (
<Wrapper>
<DropdownComponent
@ -94,19 +100,46 @@ export const WalletSummaryComponent = ({
/>
<AllAddresses value='ALL ADDRESSES' isBold />
<ValueBox>
<TextComponent size={theme.fontSize.zecValueBase * 2.5} value={`ZEC ${formatNumber(total)}`} isBold />
<USDValue value={`USD $${formatNumber(total * dollarValue)}`} size={theme.fontSize.zecValueBase * 2} />
<TextComponent
size={theme.fontSize.zecValueBase * 2.5}
value={`ZEC ${formatNumber({ value: total })}`}
isBold
/>
<USDValue
value={`USD $${formatNumber({ value: total * zecPrice })}`}
size={theme.fontSize.zecValueBase * 2}
/>
</ValueBox>
<RowComponent>
<ValueBox>
<ShieldedValue value='&#9679; SHIELDED' isBold />
<TextComponent value={`ZEC ${formatNumber(shielded)}`} isBold size={theme.fontSize.zecValueBase} />
<USDValue value={`USD $${formatNumber(shielded * dollarValue)}`} />
<ShieldedValue
value='&#9679; SHIELDED'
isBold
size={theme.fontSize.text * 0.8}
/>
<TextComponent
value={`ZEC ${formatNumber({ value: shielded })}`}
isBold
size={theme.fontSize.zecValueBase}
/>
<USDValue
value={`USD $${formatNumber({ value: shielded * zecPrice })}`}
/>
</ValueBox>
<ValueBox>
<Label value='&#9679; TRANSPARENT' isBold />
<TextComponent value={`ZEC ${formatNumber(transparent)}`} isBold size={theme.fontSize.zecValueBase} />
<USDValue value={`USD $${formatNumber(transparent * dollarValue)}`} />
<Label
value='&#9679; TRANSPARENT'
isBold
size={theme.fontSize.text * 0.8}
/>
<TextComponent
value={`ZEC ${formatNumber({ value: transparent })}`}
isBold
size={theme.fontSize.zecValueBase}
/>
<USDValue
value={`USD $${formatNumber({ value: transparent * zecPrice })}`}
/>
</ValueBox>
</RowComponent>
</Wrapper>

View File

@ -17,7 +17,13 @@ import { DoczWrapper } from '../theme.js'
<DoczWrapper>
{() => (
<div style={{ width: '700px' }}>
<WalletSummaryComponent total={5000} shielded={2500} transparent={2500} dollarValue={56} />
<WalletSummaryComponent
total={5000}
shielded={2500}
transparent={2500}
dollarValue={56}
addresses={['12345678asdaas9', '98asdasd765asd4sad321']}
/>
</div>
)}
</DoczWrapper>

View File

@ -9,6 +9,7 @@ type State = {
type Props = {};
/* eslint-disable max-len */
export const withDaemonStatusCheck = <PassedProps: {}>(
WrappedComponent: ComponentType<PassedProps>,
): ComponentType<$Diff<PassedProps, Props>> => class extends Component<PassedProps, State> {

View File

@ -25,7 +25,8 @@ export const MENU_OPTIONS = [
{
label: 'Dashboard',
route: DASHBOARD_ROUTE,
icon: (isActive: boolean) => (isActive ? DashboardIconActive : DashboardIcon),
// eslint-disable-next-line
icon: (isActive: boolean) => isActive ? DashboardIconActive : DashboardIcon,
},
{
label: 'Send',
@ -40,7 +41,8 @@ export const MENU_OPTIONS = [
{
label: 'Transactions',
route: TRANSACTIONS_ROUTE,
icon: (isActive: boolean) => (isActive ? TransactionsIconActive : TransactionsIcon),
// eslint-disable-next-line
icon: (isActive: boolean) => isActive ? TransactionsIconActive : TransactionsIcon,
},
{
label: 'Settings',

View File

@ -2,9 +2,17 @@
import { connect } from 'react-redux';
import eres from 'eres';
import flow from 'lodash.flow';
import groupBy from 'lodash.groupby';
import dateFns from 'date-fns';
import { DashboardView } from '../views/dashboard';
import rpc from '../../services/api';
import { loadWalletSummary, loadWalletSummarySuccess, loadWalletSummaryError } from '../redux/modules/wallet';
import store from '../../config/electron-store';
import {
loadWalletSummary,
loadWalletSummarySuccess,
loadWalletSummaryError,
} from '../redux/modules/wallet';
import type { AppState } from '../types/app-state';
import type { Dispatch } from '../types/redux';
@ -15,8 +23,9 @@ const mapStateToProps = ({ walletSummary }: AppState) => ({
transparent: walletSummary.transparent,
error: walletSummary.error,
isLoading: walletSummary.isLoading,
dollarValue: walletSummary.dollarValue,
zecPrice: walletSummary.zecPrice,
addresses: walletSummary.addresses,
transactions: walletSummary.transactions,
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
@ -29,14 +38,35 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
const [addressesErr, addresses] = await eres(rpc.z_listaddresses());
// eslint-disable-next-line
if (addressesErr) return dispatch(loadWalletSummaryError({ error: addressesErr.message }));
const [transactionsErr, transactions = []] = await eres(
rpc.listtransactions(),
);
if (transactionsErr) {
return dispatch(
loadWalletSummaryError({ error: transactionsErr.message }),
);
}
dispatch(
loadWalletSummarySuccess({
transparent: walletSummary.transparent,
total: walletSummary.total,
shielded: walletSummary.private,
addresses,
transactions: flow([
arr => arr.map(transaction => ({
type: transaction.category,
date: new Date(transaction.time * 1000).toISOString(),
address: transaction.address,
amount: Math.abs(transaction.amount),
})),
arr => groupBy(arr, obj => dateFns.format(obj.date, 'MMM DD, YYYY')),
])(transactions),
zecPrice: store.get('ZEC_DOLLAR_PRICE'),
}),
);
},

View File

@ -1,17 +1,5 @@
// @flow
import { connect } from 'react-redux';
import { SidebarComponent } from '../components/sidebar';
const mapStateToProps = (state: Object) => ({
todos: state.todos,
});
// const mapDispatchToProps = (dispatch: Dispatch) => ({
// addTodo: text => dispatch(addTodo(text)),
// });
export const SidebarContainer = connect(
mapStateToProps,
// mapDispatchToProps,
)(SidebarComponent);
export const SidebarContainer = SidebarComponent;

View File

@ -9,7 +9,9 @@ import { createRootReducer } from './modules/reducer';
export const history = createBrowserHistory();
const shouldEnableDevTools = (process.env.NODE_ENV !== 'production' || process.env.NODE_ENV !== 'staging') && window.devToolsExtension;
const shouldEnableDevTools = (process.env.NODE_ENV !== 'production'
|| process.env.NODE_ENV !== 'staging')
&& window.devToolsExtension;
export const configureStore = (initialState: Object) => {
const middleware = applyMiddleware(thunk, routerMiddleware(history));

View File

@ -1,5 +1,6 @@
// @flow
import type { Action } from '../../types/redux';
import type { Transaction } from '../../components/transaction-item';
// Actions
export const LOAD_WALLET_SUMMARY = 'LOAD_WALLET_SUMMARY';
@ -17,11 +18,15 @@ export const loadWalletSummarySuccess = ({
shielded,
transparent,
addresses,
transactions,
zecPrice,
}: {
total: number,
shielded: number,
transparent: number,
addresses: string[],
transactions: { [day: string]: Transaction[] },
zecPrice: number,
}) => ({
type: LOAD_WALLET_SUMMARY_SUCCESS,
payload: {
@ -29,6 +34,8 @@ export const loadWalletSummarySuccess = ({
shielded,
transparent,
addresses,
transactions,
zecPrice,
},
});
@ -43,8 +50,9 @@ export type State = {
transparent: number,
error: string | null,
isLoading: boolean,
dollarValue: number,
zecPrice: number,
addresses: [],
transactions: { [day: string]: Transaction[] },
};
const initialState = {
@ -53,8 +61,9 @@ const initialState = {
transparent: 0,
error: null,
isLoading: false,
dollarValue: 0,
zecPrice: 0,
addresses: [],
transactions: {},
};
export default (state: State = initialState, action: Action) => {

View File

@ -16,7 +16,11 @@ import { LayoutComponent } from '../components/layout';
import { HeaderComponent } from '../components/header';
import {
DASHBOARD_ROUTE, SEND_ROUTE, RECEIVE_ROUTE, SETTINGS_ROUTE, CONSOLE_ROUTE,
DASHBOARD_ROUTE,
SEND_ROUTE,
RECEIVE_ROUTE,
SETTINGS_ROUTE,
CONSOLE_ROUTE,
} from '../constants/routes';
const FullWrapper = styled.div`
@ -46,7 +50,11 @@ export const RouterComponent = ({ location }: { location: Location }) => (
{/* $FlowFixMe */}
<LayoutComponent>
<Switch>
<Route exact path={DASHBOARD_ROUTE} component={DashboardContainer} />
<Route
exact
path={DASHBOARD_ROUTE}
component={DashboardContainer}
/>
<Route path={SEND_ROUTE} component={SendView} />
<Route path={RECEIVE_ROUTE} component={ReceiveView} />
<Route path={SETTINGS_ROUTE} component={SettingsView} />

View File

@ -19,6 +19,9 @@ const sidebarLogoGradientEnd = '#FFE240';
const sidebarHoveredItem = '#1C1C1C';
const sidebarHoveredItemLabel = '#969696';
const background = '#212124';
const transactionSent = '#FF6C6C';
const transactionReceived = '#6AEAC0';
const transactionsDate = '#777777';
const appTheme = {
mode: DARK,
@ -29,7 +32,7 @@ const appTheme = {
},
fontSize: {
title: 1.25,
text: 0.9375,
text: 0.84375,
zecValueBase: 1.125,
},
colors: {
@ -53,6 +56,9 @@ const appTheme = {
sidebarLogoGradientBegin,
sidebarLogoGradientEnd,
background,
transactionSent,
transactionReceived,
transactionsDate,
},
sidebarWidth: '200px',
headerHeight: '60px',

View File

@ -0,0 +1,3 @@
// @flow
export default ({ value, append = '' }: { value: number, append?: string }) => `${append}${(value || 0).toLocaleString('de-DE')}`;

View File

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

View File

@ -3,8 +3,11 @@
import React from 'react';
import { WalletSummaryComponent } from '../components/wallet-summary';
import { TransactionDailyComponent } from '../components/transaction-daily';
import { withDaemonStatusCheck } from '../components/with-daemon-status-check';
import type { Transaction } from '../components/transaction-item';
type Props = {
getSummary: () => void,
total: number,
@ -12,8 +15,9 @@ type Props = {
transparent: number,
error: string | null,
isLoading: boolean,
dollarValue: number,
zecPrice: number,
addresses: string[],
transactions: { [day: string]: Transaction[] },
};
export class Dashboard extends React.Component<Props> {
@ -24,9 +28,18 @@ export class Dashboard extends React.Component<Props> {
render() {
const {
error, isLoading, total, shielded, transparent, dollarValue, addresses,
error,
isLoading,
total,
shielded,
transparent,
zecPrice,
addresses,
transactions,
} = this.props;
const days = Object.keys(transactions);
if (error) {
return error;
}
@ -36,13 +49,22 @@ export class Dashboard extends React.Component<Props> {
{isLoading ? (
'Loading'
) : (
<WalletSummaryComponent
total={total}
shielded={shielded}
transparent={transparent}
dollarValue={dollarValue}
addresses={addresses}
/>
<div>
<WalletSummaryComponent
total={total}
shielded={shielded}
transparent={transparent}
zecPrice={zecPrice}
addresses={addresses}
/>
{days.map(day => (
<TransactionDailyComponent
transactionsDate={day}
transactions={transactions[day]}
zecPrice={zecPrice}
/>
))}
</div>
)}
</div>
);

View File

@ -19,16 +19,36 @@ import log from './logger';
const queue = new Queue({ concurrency: 1, autoStart: false });
const httpClient = got.extend({ baseUrl: 'https://z.cash/downloads/', retry: 3, useElectronNet: true });
const httpClient = got.extend({
baseUrl: 'https://z.cash/downloads/',
retry: 3,
useElectronNet: true,
});
const FILES: Array<{ name: string, hash: string }> = [
{ name: 'sprout-proving.key', hash: '8bc20a7f013b2b58970cddd2e7ea028975c88ae7ceb9259a5344a16bc2c0eef7' },
{ name: 'sprout-verifying.key', hash: '4bd498dae0aacfd8e98dc306338d017d9c08dd0918ead18172bd0aec2fc5df82' },
{ name: 'sapling-spend.params', hash: '8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13' },
{ name: 'sapling-output.params', hash: '2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4' },
{ name: 'sprout-groth16.params', hash: 'b685d700c60328498fbde589c8c7c484c722b788b265b72af448a5bf0ee55b50' },
{
name: 'sprout-proving.key',
hash: '8bc20a7f013b2b58970cddd2e7ea028975c88ae7ceb9259a5344a16bc2c0eef7',
},
{
name: 'sprout-verifying.key',
hash: '4bd498dae0aacfd8e98dc306338d017d9c08dd0918ead18172bd0aec2fc5df82',
},
{
name: 'sapling-spend.params',
hash: '8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13',
},
{
name: 'sapling-output.params',
hash: '2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4',
},
{
name: 'sprout-groth16.params',
hash: 'b685d700c60328498fbde589c8c7c484c722b788b265b72af448a5bf0ee55b50',
},
];
// eslint-disable-next-line max-len
const checkSha256 = (pathToFile: string, expectedHash: string) => new Promise((resolve, reject) => {
fs.readFile(pathToFile, (err, file) => {
if (err) return reject(new Error(err));
@ -39,6 +59,7 @@ const checkSha256 = (pathToFile: string, expectedHash: string) => new Promise((r
});
});
// eslint-disable-next-line max-len
const downloadFile = ({ file, pathToSave }): Promise<*> => new Promise((resolve, reject) => {
log(`Downloading ${file.name}...`);
@ -50,7 +71,9 @@ const downloadFile = ({ file, pathToSave }): Promise<*> => new Promise((resolve,
log(`SHA256 validation for file ${file.name} succeeded!`);
resolve(file.name);
} else {
reject(new Error(`SHA256 validation failed for file: ${file.name}`));
reject(
new Error(`SHA256 validation failed for file: ${file.name}`),
);
}
});
})
@ -61,7 +84,9 @@ const downloadFile = ({ file, pathToSave }): Promise<*> => new Promise((resolve,
let missingDownloadParam = false;
export default (): Promise<*> => new Promise((resolve, reject) => {
const firstRunProcess = cp.spawn(path.join(getBinariesPath(), 'win', 'first-run.bat'));
const firstRunProcess = cp.spawn(
path.join(getBinariesPath(), 'win', 'first-run.bat'),
);
firstRunProcess.stdout.on('data', data => log(data.toString()));
firstRunProcess.stderr.on('data', data => reject(data.toString()));
@ -70,20 +95,33 @@ export default (): Promise<*> => new Promise((resolve, reject) => {
await Promise.all(
FILES.map(async (file) => {
const pathToSave = path.join(app.getPath('userData'), '..', 'ZcashParams', file.name);
const pathToSave = path.join(
app.getPath('userData'),
'..',
'ZcashParams',
file.name,
);
const [cannotAccess] = await eres(util.promisify(fs.access)(pathToSave, fs.constants.F_OK));
const [cannotAccess] = await eres(
util.promisify(fs.access)(pathToSave, fs.constants.F_OK),
);
if (cannotAccess) {
missingDownloadParam = true;
// eslint-disable-next-line max-len
queue.add(() => downloadFile({ file, pathToSave }).then(() => log(`Download ${file.name} finished!`)));
} else {
const isValid = await checkSha256(pathToSave, file.hash);
if (isValid) {
log(`${file.name} already is in ${pathToSave}...`);
} else {
log(`File: ${file.name} failed in the SHASUM validation, downloading again...`);
log(
`File: ${
file.name
} failed in the SHASUM validation, downloading again...`,
);
queue.add(() => {
// eslint-disable-next-line max-len
downloadFile({ file, pathToSave }).then(() => log(`Download ${file.name} finished!`));
});
}

View File

@ -3,5 +3,8 @@ import path from 'path';
/* eslint-disable-next-line import/no-extraneous-dependencies */
import isDev from 'electron-is-dev';
// $FlowFixMe
export default () => (isDev ? path.join(__dirname, '..', '..', './bin') : path.join(process.resourcesPath, 'bin'));
/* eslint-disable operator-linebreak */
export default () => (isDev
? path.join(__dirname, '..', '..', './bin')
: // $FlowFixMe
path.join(process.resourcesPath, 'bin'));

View File

@ -8,5 +8,7 @@ import runUnixFetchParams from './fetch-unix-params';
export default (): Promise<*> => {
log('Fetching params');
return os.platform() === 'win32' ? fetchWindowsParams() : runUnixFetchParams();
return os.platform() === 'win32'
? fetchWindowsParams()
: runUnixFetchParams();
};

View File

@ -35,13 +35,20 @@ const getDaemonOptions = ({ username, password }) => {
`-rpcuser=${username}`,
`-rpcpassword=${password}`,
];
return isDev ? defaultOptions.concat(['-testnet', '-addnode=testnet.z.cash']) : defaultOptions;
return isDev
? defaultOptions.concat(['-testnet', '-addnode=testnet.z.cash'])
: defaultOptions;
};
let resolved = false;
// eslint-disable-next-line
const runDaemon: () => Promise<?ChildProcess> = () => new Promise(async (resolve, reject) => {
const processName = path.join(getBinariesPath(), getOsFolder(), getDaemonName());
const processName = path.join(
getBinariesPath(),
getOsFolder(),
getDaemonName(),
);
const [err] = await eres(fetchParams());
@ -76,9 +83,13 @@ const runDaemon: () => Promise<?ChildProcess> = () => new Promise(async (resolve
store.set('rpcpassword', rpcCredentials.password);
}
const childProcess = cp.spawn(processName, getDaemonOptions(rpcCredentials), {
stdio: ['ignore', 'pipe', 'pipe'],
});
const childProcess = cp.spawn(
processName,
getDaemonOptions(rpcCredentials),
{
stdio: ['ignore', 'pipe', 'pipe'],
},
);
childProcess.stdout.on('data', (data) => {
if (mainWindow) mainWindow.webContents.send('zcashd-log', data.toString());

View File

@ -13,6 +13,8 @@ import eres from 'eres';
import { registerDebugShortcut } from '../utils/debug-shortcut';
import runDaemon from './daemon/zcashd-child-process';
import zcashLog from './daemon/logger';
import getZecPrice from '../services/zec-price';
import store from './electron-store';
let mainWindow: BrowserWindowType;
let updateAvailable: boolean = false;
@ -37,9 +39,9 @@ const createWindow = () => {
autoUpdater.on('download-progress', progress => showStatus(
/* eslint-disable-next-line max-len */
`Download speed: ${progress.bytesPerSecond} - Downloaded ${progress.percent}% (${progress.transferred}/${
progress.total
})`,
`Download speed: ${progress.bytesPerSecond} - Downloaded ${
progress.percent
}% (${progress.transferred}/${progress.total})`,
));
autoUpdater.on('update-downloaded', () => {
updateAvailable = true;
@ -58,10 +60,18 @@ const createWindow = () => {
},
});
getZecPrice().then((obj) => {
store.set('ZEC_DOLLAR_PRICE', obj.USD);
});
mainWindow.setVisibleOnAllWorkspaces(true);
registerDebugShortcut(app, mainWindow);
mainWindow.loadURL(isDev ? 'http://0.0.0.0:8080/' : `file://${path.join(__dirname, '../build/index.html')}`);
mainWindow.loadURL(
isDev
? 'http://0.0.0.0:8080/'
: `file://${path.join(__dirname, '../build/index.html')}`,
);
exports.app = app;
exports.mainWindow = mainWindow;

View File

@ -45,7 +45,6 @@
"css-loader": "^1.0.1",
"docz": "^0.12.13",
"docz-plugin-css": "^0.11.0",
"electron": "^3.0.10",
"electron-builder": "^20.36.2",
"electron-icon-maker": "^0.0.4",
"electron-is-dev": "^1.0.1",
@ -88,10 +87,14 @@
"@babel/register": "^7.0.0",
"autoprefixer": "^9.3.1",
"connected-react-router": "^5.0.1",
"date-fns": "^1.30.1",
"electron": "^3.0.10",
"electron-store": "^2.0.0",
"eres": "^1.0.1",
"got": "^9.3.2",
"history": "^4.7.2",
"lodash.flow": "^3.5.0",
"lodash.groupby": "^4.6.0",
"p-queue": "^3.0.0",
"process-exists": "^3.1.0",
"qrcode.react": "^0.8.0",

View File

@ -745,8 +745,8 @@ export type APIMethods = {
z_exportwallet: (filename: string) => Promise<string>,
z_getbalance: (address: string, minconf?: number) => Promise<number>,
z_getnewaddress: (type: string) => Promise<string>,
z_getoperationresult: (operationid: string) => Promise<Object[]>,
z_getoperationstatus: (operationid: string) => Promise<Object[]>,
z_getoperationresult: (operationid?: string[]) => Promise<Object[]>,
z_getoperationstatus: (operationid?: string[]) => Promise<Object[]>,
z_gettotalbalance: (
minconf?: number,
includeWatchonly?: boolean,

31
services/zec-price.js Normal file
View File

@ -0,0 +1,31 @@
// @flow
import { net } from 'electron';
type Payload = {
[currency: string]: number,
};
/**
WARNING:
Just a super fast way to get the zec price
*/
export default (currencies: string[] = ['USD']): Promise<Payload> => new Promise((resolve, reject) => {
const ENDPOINT = `https://min-api.cryptocompare.com/data/price?fsym=ZEC&tsyms=${currencies.join(
',',
)}&api_key=b6162b068ff9f8fe2872070b791146b06d186e83d5e52e49dcaa42ef8d1d3875`;
const request = net.request(ENDPOINT);
request.on('response', (response) => {
let data = '';
/* eslint-disable-next-line no-return-assign */
response.on('data', chunk => (data += chunk));
response.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (err) {
reject(err);
}
});
});
request.end();
});

View File

@ -4080,6 +4080,11 @@ date-fns@^1.23.0:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6"
integrity sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw==
date-fns@^1.30.1:
version "1.30.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
@ -8866,11 +8871,21 @@ lodash.flattendepth@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash.flattendepth/-/lodash.flattendepth-4.7.0.tgz#b4d2d14fc7d9c53deb96642eb616fff22a60932f"
integrity sha1-tNLRT8fZxT3rlmQuthb/8ipgky8=
lodash.flow@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a"
integrity sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o=
lodash.get@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
lodash.groupby@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.groupby/-/lodash.groupby-4.6.0.tgz#0b08a1dcf68397c397855c3239783832df7403d1"
integrity sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E=
lodash.isarguments@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"