Add RSK network w/ network agnostic refactors (#1939)

* Rsk network with checksum

* Initial change from chainid everywhere to selectors

* Fix ternary

* Check in address book changes to lower case. Currently a little busted.

* Fix validation

* Fix tests.

* Identicon back to SFC

* Remove unnecessary prop

* Paper Wallet Refactor (#1950)

* Convert print wallet to save png image. Move all styles into CSS.

* Fix re-print paper wallet with save modal

* Address PR comments
This commit is contained in:
William O'Beirne 2018-06-15 19:28:42 -04:00 committed by Daniel Ternyak
parent 4b45f813b3
commit 994fa03828
54 changed files with 743 additions and 454 deletions

View File

@ -20,7 +20,7 @@ export function setAddressLabel(payload: AddressLabel): SetAddressLabel {
}
export type TClearAddressLabel = typeof clearAddressLabel;
export function clearAddressLabel(payload: string): ClearAddressLabel {
export function clearAddressLabel(payload: AddressLabel['address']): ClearAddressLabel {
return {
type: TypeKeys.CLEAR_ADDRESS_LABEL,
payload
@ -44,7 +44,7 @@ export function changeAddressLabelEntry(payload: AddressLabelEntry): ChangeAddre
}
export type TSaveAddressLabelEntry = typeof saveAddressLabelEntry;
export function saveAddressLabelEntry(payload: string): SaveAddressLabelEntry {
export function saveAddressLabelEntry(payload: AddressLabelEntry['id']): SaveAddressLabelEntry {
return {
type: TypeKeys.SAVE_ADDRESS_LABEL_ENTRY,
payload
@ -52,7 +52,7 @@ export function saveAddressLabelEntry(payload: string): SaveAddressLabelEntry {
}
export type TClearAddressLabelEntry = typeof clearAddressLabelEntry;
export function clearAddressLabelEntry(payload: string): ClearAddressLabelEntry {
export function clearAddressLabelEntry(payload: AddressLabelEntry['id']): ClearAddressLabelEntry {
return {
type: TypeKeys.CLEAR_ADDRESS_LABEL_ENTRY,
payload
@ -60,7 +60,7 @@ export function clearAddressLabelEntry(payload: string): ClearAddressLabelEntry
}
export type TRemoveAddressLabelEntry = typeof removeAddressLabelEntry;
export function removeAddressLabelEntry(payload: string): RemoveAddressLabelEntry {
export function removeAddressLabelEntry(payload: AddressLabelEntry['id']): RemoveAddressLabelEntry {
return {
type: TypeKeys.REMOVE_ADDRESS_LABEL_ENTRY,
payload

View File

@ -17,6 +17,7 @@ import {
getAddressLabelRows,
getAddressBookTableEntry
} from 'selectors/addressBook';
import { getChecksumAddressFn } from 'selectors/config';
import { Input, Identicon } from 'components/ui';
import AddressBookTableRow from './AddressBookTableRow';
import './AddressBookTable.scss';
@ -32,6 +33,7 @@ interface StateProps {
entry: ReturnType<typeof getAddressBookTableEntry>;
addressLabels: ReturnType<typeof getAddressLabels>;
labelAddresses: ReturnType<typeof getLabelAddresses>;
toChecksumAddress: ReturnType<typeof getChecksumAddressFn>;
}
type Props = DispatchProps & StateProps;
@ -200,7 +202,9 @@ class AddressBookTable extends React.Component<Props, State> {
private makeLabelRow = (row: any, index: number) => {
const { editingRow } = this.state;
const { id, address, label, temporaryLabel, labelError } = row;
const { id, label, temporaryLabel, labelError } = row;
const address = this.props.toChecksumAddress(row.address);
const isEditing = index === editingRow;
const onChange = (newLabel: string) =>
this.props.changeAddressLabelEntry({
@ -308,7 +312,8 @@ const mapStateToProps: MapStateToProps<StateProps, {}, AppState> = state => ({
rows: getAddressLabelRows(state),
entry: getAddressBookTableEntry(state),
addressLabels: getAddressLabels(state),
labelAddresses: getLabelAddresses(state)
labelAddresses: getLabelAddresses(state),
toChecksumAddress: getChecksumAddressFn(state)
});
const mapDispatchToProps: DispatchProps = {

View File

@ -1,22 +1,31 @@
import React from 'react';
import { connect } from 'react-redux';
import { AddressFieldFactory } from './AddressFieldFactory';
import { donationAddressMap } from 'config';
import translate from 'translations';
import { Input } from 'components/ui';
import { toChecksumAddress } from 'ethereumjs-util';
import { getChecksumAddressFn } from 'selectors/config';
import { AppState } from 'reducers';
interface Props {
interface OwnProps {
isReadOnly?: boolean;
isSelfAddress?: boolean;
isCheckSummed?: boolean;
showLabelMatch?: boolean;
}
export const AddressField: React.SFC<Props> = ({
interface StateProps {
toChecksumAddress: ReturnType<typeof getChecksumAddressFn>;
}
type Props = OwnProps & StateProps;
const AddressField: React.SFC<Props> = ({
isReadOnly,
isSelfAddress,
isCheckSummed,
showLabelMatch
showLabelMatch,
toChecksumAddress
}) => (
<AddressFieldFactory
isSelfAddress={isSelfAddress}
@ -44,3 +53,7 @@ export const AddressField: React.SFC<Props> = ({
)}
/>
);
export default connect((state: AppState): StateProps => ({
toChecksumAddress: getChecksumAddressFn(state)
}))(AddressField);

View File

@ -11,12 +11,12 @@ import {
removeAddressLabelEntry,
TRemoveAddressLabelEntry
} from 'actions/addressBook';
import { getAccountAddressEntry, getAddressLabels } from 'selectors/addressBook';
import { getAccountAddressEntry, getAddressLabelEntryFromAddress } from 'selectors/addressBook';
import { Address, Identicon, Input } from 'components/ui';
interface StateProps {
entry: ReturnType<typeof getAccountAddressEntry>;
addressLabels: ReturnType<typeof getAddressLabels>;
addressLabel: string;
}
interface DispatchProps {
@ -65,13 +65,12 @@ class AccountAddress extends React.Component<Props, State> {
}
public render() {
const { address, addressLabels } = this.props;
const { address, addressLabel } = this.props;
const { copied } = this.state;
const label = addressLabels[address];
const labelContent = this.generateLabelContent();
const labelButton = this.generateLabelButton();
const addressClassName = `AccountInfo-address-addr ${
label ? 'AccountInfo-address-addr--small' : ''
addressLabel ? 'AccountInfo-address-addr--small' : ''
}`;
return (
@ -120,10 +119,9 @@ class AccountAddress extends React.Component<Props, State> {
private setLabelInputRef = (node: HTMLInputElement) => (this.labelInput = node);
private generateLabelContent = () => {
const { address, addressLabels, entry: { temporaryLabel, labelError } } = this.props;
const { addressLabel, entry: { temporaryLabel, labelError } } = this.props;
const { editingLabel, labelInputTouched } = this.state;
const storedLabel = addressLabels[address];
const newLabelSameAsPrevious = temporaryLabel === storedLabel;
const newLabelSameAsPrevious = temporaryLabel === addressLabel;
const labelInputTouchedWithError = labelInputTouched && !newLabelSameAsPrevious && labelError;
let labelContent = null;
@ -134,7 +132,7 @@ class AccountAddress extends React.Component<Props, State> {
<Input
title={translateRaw('ADD_LABEL')}
placeholder={translateRaw('NEW_LABEL')}
defaultValue={storedLabel}
defaultValue={addressLabel}
onChange={this.handleLabelChange}
onKeyDown={this.handleKeyDown}
onFocus={this.setTemporaryLabelTouched}
@ -149,20 +147,15 @@ class AccountAddress extends React.Component<Props, State> {
</React.Fragment>
);
} else {
labelContent = (
<label title={storedLabel} className="AccountInfo-address-label">
{storedLabel}
</label>
);
labelContent = <label className="AccountInfo-address-label">{addressLabel}</label>;
}
return labelContent;
};
private generateLabelButton = () => {
const { address, addressLabels } = this.props;
const { addressLabel } = this.props;
const { editingLabel } = this.state;
const label = addressLabels[address];
const labelButton = editingLabel ? (
<React.Fragment>
<i className="fa fa-save" />
@ -175,10 +168,10 @@ class AccountAddress extends React.Component<Props, State> {
<i className="fa fa-pencil" />
<span
role="button"
title={label ? translateRaw('EDIT_LABEL') : translateRaw('ADD_LABEL_9')}
title={addressLabel ? translateRaw('EDIT_LABEL') : translateRaw('ADD_LABEL_9')}
onClick={this.startEditingLabel}
>
{label ? translate('EDIT_LABEL') : translate('ADD_LABEL_9')}
{addressLabel ? translate('EDIT_LABEL') : translate('ADD_LABEL_9')}
</span>
</React.Fragment>
);
@ -187,13 +180,12 @@ class AccountAddress extends React.Component<Props, State> {
};
private handleBlur = () => {
const { address, addressLabels, entry: { id, label, temporaryLabel, labelError } } = this.props;
const storedLabel = addressLabels[address];
const { address, addressLabel, entry: { id, label, temporaryLabel, labelError } } = this.props;
this.clearTemporaryLabelTouched();
this.stopEditingLabel();
if (temporaryLabel === storedLabel) {
if (temporaryLabel === addressLabel) {
return;
}
@ -255,10 +247,16 @@ class AccountAddress extends React.Component<Props, State> {
private clearTemporaryLabelTouched = () => this.setState({ labelInputTouched: false });
}
const mapStateToProps: MapStateToProps<StateProps, {}, AppState> = (state: AppState) => ({
entry: getAccountAddressEntry(state),
addressLabels: getAddressLabels(state)
});
const mapStateToProps: MapStateToProps<StateProps, {}, AppState> = (
state: AppState,
ownProps: OwnProps
) => {
const labelEntry = getAddressLabelEntryFromAddress(state, ownProps.address);
return {
entry: getAccountAddressEntry(state),
addressLabel: labelEntry ? labelEntry.label : ''
};
};
const mapDispatchToProps: DispatchProps = {
changeAddressLabelEntry,

View File

@ -1,11 +1,10 @@
import React from 'react';
import { connect } from 'react-redux';
import { toChecksumAddress } from 'ethereumjs-util';
import { UnitDisplay, NewTabLink } from 'components/ui';
import { IWallet, HardwareWallet, Balance } from 'libs/wallet';
import translate, { translateRaw } from 'translations';
import Spinner from 'components/ui/Spinner';
import { getNetworkConfig, getOffline } from 'selectors/config';
import { getNetworkConfig, getOffline, getChecksumAddressFn } from 'selectors/config';
import { AppState } from 'reducers';
import { NetworkConfig } from 'types/network';
import { TRefreshAccountBalance, refreshAccountBalance } from 'actions/wallet';
@ -21,6 +20,7 @@ interface StateProps {
balance: Balance;
network: ReturnType<typeof getNetworkConfig>;
isOffline: ReturnType<typeof getOffline>;
toChecksumAddress: ReturnType<typeof getChecksumAddressFn>;
}
interface State {
@ -73,7 +73,7 @@ class AccountInfo extends React.Component<Props, State> {
};
public render() {
const { network, isOffline, balance, wallet } = this.props;
const { network, isOffline, balance, toChecksumAddress, wallet } = this.props;
const { address, showLongBalance, confirmAddr } = this.state;
let blockExplorer;
@ -199,7 +199,8 @@ function mapStateToProps(state: AppState): StateProps {
return {
balance: state.wallet.balance,
network: getNetworkConfig(state),
isOffline: getOffline(state)
isOffline: getOffline(state),
toChecksumAddress: getChecksumAddressFn(state)
};
}
const mapDispatchToProps: DispatchProps = { refreshAccountBalance };

View File

@ -8,21 +8,21 @@ import { connect } from 'react-redux';
import { SerializedTransaction } from 'components/renderCbs';
import { AppState } from 'reducers';
import { getFrom, getUnit, isEtherTransaction } from 'selectors/transaction';
import { toChecksumAddress } from 'ethereumjs-util';
import translate from 'translations';
import { getChecksumAddressFn } from 'selectors/config';
interface StateProps {
from: AppState['transaction']['meta']['from'];
unit: AppState['transaction']['meta']['unit'];
from: ReturnType<typeof getFrom>;
unit: ReturnType<typeof getUnit>;
isToken: boolean;
toChecksumAddress: ReturnType<typeof getChecksumAddressFn>;
}
const size = '3rem';
class AddressesClass extends Component<StateProps> {
public render() {
const { from, isToken, unit } = this.props;
const { from, isToken, unit, toChecksumAddress } = this.props;
return (
<SerializedTransaction
withSerializedTransaction={(_, { to, data }) => {
@ -92,7 +92,8 @@ class AddressesClass extends Component<StateProps> {
const mapStateToProps = (state: AppState): StateProps => ({
from: getFrom(state),
isToken: !isEtherTransaction(state),
unit: getUnit(state)
unit: getUnit(state),
toChecksumAddress: getChecksumAddressFn(state)
});
export const Addresses = connect(mapStateToProps)(AddressesClass);

View File

@ -0,0 +1,89 @@
// Use px and don't use variables here, we want the wallet to look
// consistent regardless of theme or screen size.
$wallet-width: 680px;
$wallet-height: 280px;
.PaperWallet {
position: relative;
margin: 0 auto;
width: $wallet-width;
height: $wallet-height;
border: 1px solid #163151;
user-select: none;
cursor: default;
&-sidebar {
float: left;
height: 100%;
width: auto;
}
&-block {
position: relative;
float: left;
width: 27.5%;
padding: 20px;
&-box {
width: 150px;
height: 150px;
&.is-shaded {
background: rgba(#000, 0.02);
}
}
&-text {
position: absolute;
top: 50%;
left: 100%;
width: 100%;
margin: 0;
transform: translate(-50%, -50%) rotate(-90deg);
font-size: 13px;
font-weight: 600;
color: #0b7290;
text-align: center;
text-transform: uppercase;
letter-spacing: 1px;
}
}
&-info {
float: left;
width: 85%;
padding: 0 20px;
&-text {
margin: 0 0 5px;
text-align: left;
font-size: 14px;
font-family: "Roboto Mono", "Menlo", "Monaco", "Consolas", "Courier New", monospace;
font-weight: 300;
&-label {
font-weight: 600;
}
}
}
&-identicon {
position: absolute;
right: 15px;
bottom: 45px;
&-left {
float: left;
}
&-text {
float: left;
width: 130px;
padding: 0 5px;
margin: 12px 0 0;
font-size: 9px;
text-align: center;
}
}
}

View File

@ -1,94 +1,10 @@
import { Identicon, QRCode } from 'components/ui';
import React from 'react';
import html2canvas from 'html2canvas';
import { addHexPrefix, toChecksumAddress } from 'ethereumjs-util';
import ethLogo from 'assets/images/logo-ethereum-1.png';
import { Identicon, QRCode } from 'components/ui';
import notesBg from 'assets/images/notes-bg.png';
import sidebarImg from 'assets/images/print-sidebar.png';
const walletWidth = 680;
const walletHeight = 280;
const styles: any = {
container: {
position: 'relative',
margin: '0 auto',
width: `${walletWidth}px`,
height: `${walletHeight}px`,
border: '1px solid #163151',
userSelect: 'none',
cursor: 'default'
},
// Images
sidebar: {
float: 'left',
height: '100%',
width: 'auto'
},
ethLogo: {
position: 'absolute',
left: '86px',
height: '100%',
width: 'auto',
zIndex: '-1'
},
// Blocks / QR Codes
block: {
position: 'relative',
float: 'left',
width: '27.5%',
padding: '20px'
},
blockText: {
position: 'absolute',
top: '50%',
left: '100%',
width: '100%',
margin: 0,
transform: 'translate(-50%, -50%) rotate(-90deg)',
fontSize: '13px',
fontWeight: '600',
color: '#0b7290',
textAlign: 'center',
textTransform: 'uppercase',
letterSpacing: '1px'
},
// Address / private key info
infoContainer: {
float: 'left',
width: '85%',
padding: '0 20px'
},
infoText: {
margin: '0 0 8px',
textAlign: 'left',
fontSize: '14px',
fontFamily: '"Roboto Mono", Menlo, Monaco, Consolas, "Courier New", monospace',
fontWeight: 300
},
infoLabel: {
fontWeight: 600
},
identiconContainer: {
position: 'absolute',
right: '15px',
bottom: '45px'
},
identiconText: {
float: 'left',
width: '130px',
padding: '0 5px',
margin: '12px 0 0',
fontSize: '9px',
textAlign: 'center'
},
box: {
width: 150,
height: 150
}
};
import './index.scss';
interface Props {
address: string;
@ -96,54 +12,65 @@ interface Props {
}
export default class PaperWallet extends React.Component<Props, {}> {
private container: HTMLElement | null;
public render() {
const { privateKey } = this.props;
const address = toChecksumAddress(addHexPrefix(this.props.address));
return (
<div style={styles.container}>
<img src={sidebarImg} style={styles.sidebar} alt="MyCrypto Logo" />
<img src={ethLogo} style={styles.ethLogo} alt="ETH Logo" />
<div className="PaperWallet" ref={el => (this.container = el)}>
<img src={sidebarImg} className="PaperWallet-sidebar" alt="MyCrypto Logo" />
<div style={styles.block}>
<div style={styles.box}>
<div className="PaperWallet-block">
<div className="PaperWallet-block-box">
<QRCode data={address} />
</div>
<p style={styles.blockText}>YOUR ADDRESS</p>
<p className="PaperWallet-block-text">YOUR ADDRESS</p>
</div>
<div style={styles.block}>
<img src={notesBg} style={styles.box} aria-hidden={true} />
<p style={styles.blockText}>AMOUNT / NOTES</p>
<div className="PaperWallet-block">
<img src={notesBg} className="PaperWallet-block-box is-shaded" aria-hidden={true} />
<p className="PaperWallet-block-text">AMOUNT / NOTES</p>
</div>
<div style={styles.block}>
<div style={styles.box}>
<div className="PaperWallet-block">
<div className="PaperWallet-block-box">
<QRCode data={privateKey} />
</div>
<p style={styles.blockText}>YOUR PRIVATE KEY</p>
<p className="PaperWallet-block-text">YOUR PRIVATE KEY</p>
</div>
<div style={styles.infoContainer}>
<p style={styles.infoText}>
<strong style={styles.infoLabel}>Your Address:</strong>
<div className="PaperWallet-info">
<p className="PaperWallet-info-text">
<strong className="PaperWallet-info-text-label">Your Address:</strong>
<br />
{address}
</p>
<p style={styles.infoText}>
<strong style={styles.infoLabel}>Your Private Key:</strong>
<p className="PaperWallet-info-text">
<strong className="PaperWallet-info-text-label">Your Private Key:</strong>
<br />
{privateKey}
</p>
</div>
<div style={styles.identiconContainer}>
<div style={{ float: 'left' }}>
<div className="PaperWallet-identicon">
<div className="PaperWallet-identicon-left">
<Identicon address={address} size={'42px'} />
</div>
<p style={styles.identiconText}>Always look for this icon when sending to this wallet</p>
<p className="PaperWallet-identicon-text">
Always look for this icon when sending to this wallet
</p>
</div>
</div>
);
}
public toPNG = async () => {
if (!this.container) {
return '';
}
const canvas = await html2canvas(this.container);
return canvas.toDataURL('image/png');
};
}

View File

@ -1,53 +1,53 @@
import { PaperWallet } from 'components';
import React from 'react';
import printElement from 'utils/printElement';
import { PaperWallet } from 'components';
import { stripHexPrefix } from 'libs/values';
import translate, { translateRaw } from 'translations';
export const print = (address: string, privateKey: string) => () =>
address &&
privateKey &&
printElement(<PaperWallet address={address} privateKey={privateKey} />, {
popupFeatures: {
scrollbars: 'no'
},
styles: `
* {
box-sizing: border-box;
}
body {
font-family: Lato, sans-serif;
font-size: 1rem;
line-height: 1.4;
margin: 0;
}
`
});
import translate from 'translations';
interface Props {
address: string;
privateKey: string;
}
const PrintableWallet: React.SFC<Props> = ({ address, privateKey }) => {
const pkey = stripHexPrefix(privateKey);
interface State {
paperWalletImage: string;
}
return (
<div>
<PaperWallet address={address} privateKey={pkey} />
<a
role="button"
aria-label={translateRaw('X_PRINT')}
aria-describedby="x_PrintDesc"
className="btn btn-lg btn-primary btn-block"
onClick={print(address, pkey)}
style={{ margin: '10px auto 0', maxWidth: '260px' }}
>
{translate('X_PRINT')}
</a>
</div>
);
};
export default class PrintableWallet extends React.Component<Props, State> {
public state: State = {
paperWalletImage: ''
};
export default PrintableWallet;
private paperWallet: PaperWallet | null;
public componentDidMount() {
setTimeout(() => {
if (!this.paperWallet) {
return this.componentDidMount();
}
this.paperWallet.toPNG().then(png => this.setState({ paperWalletImage: png }));
}, 500);
}
public render() {
const { address, privateKey } = this.props;
const { paperWalletImage } = this.state;
const pkey = stripHexPrefix(privateKey);
const disabled = paperWalletImage ? '' : 'disabled';
return (
<div>
<PaperWallet address={address} privateKey={pkey} ref={c => (this.paperWallet = c)} />
<a
role="button"
href={paperWalletImage}
className={`btn btn-lg btn-primary btn-block ${disabled}`}
style={{ margin: '10px auto 0', maxWidth: '260px' }}
download={`paper-wallet-0x${address.substr(0, 6)}`}
>
{translate('X_SAVE_PAPER')}
</a>
</div>
);
}
}

View File

@ -1,7 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import Select, { Option } from 'react-select';
import { toChecksumAddress } from 'ethereumjs-util';
import translate, { translateRaw } from 'translations';
import {
DeterministicWalletData,
@ -201,14 +200,13 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
private getAddresses(props: Props = this.props) {
const { dPath, publicKey, chainCode, seed } = props;
if (dPath && ((publicKey && chainCode) || seed)) {
if (isValidPath(dPath.value)) {
this.props.getDeterministicWallets({
seed,
dPath: dPath.value,
publicKey,
chainCode,
dPath: dPath.value,
limit: WALLETS_PER_PAGE,
offset: WALLETS_PER_PAGE * this.state.page
});
@ -280,7 +278,7 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
private renderWalletRow(wallet: DeterministicWalletData) {
const { desiredToken, network, addressLabels } = this.props;
const { selectedAddress } = this.state;
const label = addressLabels[toChecksumAddress(wallet.address)];
const label = addressLabels[wallet.address.toLowerCase()];
const spanClassName = label ? 'DWModal-addresses-table-address-text' : '';
// Get renderable values, but keep 'em short

View File

@ -2,19 +2,20 @@ import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import Select, { Option } from 'react-select';
import translate, { translateRaw } from 'translations';
import { isValidETHAddress } from 'libs/validators';
import { AddressOnlyWallet } from 'libs/wallet';
import { getRecentAddresses } from 'selectors/wallet';
import { AppState } from 'reducers';
import { Input, Identicon } from 'components/ui';
import './ViewOnly.scss';
import { getIsValidAddressFn } from 'selectors/config';
interface OwnProps {
onUnlock(param: any): void;
}
interface StateProps {
recentAddresses: AppState['wallet']['recentAddresses'];
recentAddresses: ReturnType<typeof getRecentAddresses>;
isValidAddress: ReturnType<typeof getIsValidAddressFn>;
}
type Props = OwnProps & StateProps;
@ -29,9 +30,9 @@ class ViewOnlyDecryptClass extends PureComponent<Props, State> {
};
public render() {
const { recentAddresses } = this.props;
const { recentAddresses, isValidAddress } = this.props;
const { address } = this.state;
const isValid = isValidETHAddress(address);
const isValid = isValidAddress(address);
const recentOptions = (recentAddresses.map(addr => ({
label: (
@ -89,8 +90,9 @@ class ViewOnlyDecryptClass extends PureComponent<Props, State> {
ev.preventDefault();
}
const { isValidAddress } = this.props;
const { address } = this.state;
if (isValidETHAddress(address)) {
if (isValidAddress(address)) {
const wallet = new AddressOnlyWallet(address);
this.props.onUnlock(wallet);
}
@ -98,5 +100,6 @@ class ViewOnlyDecryptClass extends PureComponent<Props, State> {
}
export const ViewOnlyDecrypt = connect((state: AppState): StateProps => ({
recentAddresses: getRecentAddresses(state)
recentAddresses: getRecentAddresses(state),
isValidAddress: getIsValidAddressFn(state)
}))(ViewOnlyDecryptClass);

View File

@ -1,4 +1,3 @@
export * from './AddressField';
export * from './DataField';
export * from './GasLimitField';
export * from './AmountField';
@ -9,11 +8,13 @@ export * from './GenerateTransaction';
export * from './SendButton';
export * from './SigningStatus';
export * from '../containers/Tabs/ScheduleTransaction/components';
export { default as AddressField } from './AddressField';
export { default as NonceField } from './NonceField';
export { default as Header } from './Header';
export { default as Footer } from './Footer';
export { default as BalanceSidebar } from './BalanceSidebar';
export { default as PaperWallet } from './PaperWallet';
export { default as PrintableWallet } from './PrintableWallet';
export { default as TXMetaDataPanel } from './TXMetaDataPanel';
export { default as WalletDecrypt } from './WalletDecrypt';
export { default as TogglablePassword } from './TogglablePassword';

View File

@ -1,40 +1,42 @@
import React from 'react';
import { toChecksumAddress } from 'ethereumjs-util';
import NewTabLink from './NewTabLink';
import { IWallet } from 'libs/wallet';
import { BlockExplorerConfig } from 'types/network';
import { getChecksumAddressFn } from 'selectors/config';
import { AppState } from 'reducers';
import { connect } from 'react-redux';
interface BaseProps {
explorer?: BlockExplorerConfig | null;
address?: string | null;
wallet?: IWallet | null;
}
interface AddressProps extends BaseProps {
address: string;
interface StateProps {
toChecksumAddress: ReturnType<typeof getChecksumAddressFn>;
}
interface WalletProps extends BaseProps {
wallet: IWallet;
}
type Props = BaseProps & StateProps;
type Props = AddressProps | WalletProps;
export class Address extends React.PureComponent<Props> {
public render() {
const { wallet, address, explorer, toChecksumAddress } = this.props;
let renderAddress = '';
if (address !== null && address !== undefined) {
renderAddress = address;
} else {
renderAddress = wallet !== null && wallet !== undefined ? wallet.getAddressString() : '';
}
renderAddress = toChecksumAddress(renderAddress);
const isAddressProps = (props: Props): props is AddressProps =>
typeof (props as AddressProps).address === 'string';
const Address: React.SFC<Props> = props => {
let addr = '';
if (isAddressProps(props)) {
addr = props.address;
} else {
addr = props.wallet.getAddressString();
if (explorer) {
return <NewTabLink href={explorer.addressUrl(renderAddress)}>{renderAddress}</NewTabLink>;
} else {
return <React.Fragment>{renderAddress}</React.Fragment>;
}
}
addr = toChecksumAddress(addr);
}
if (props.explorer) {
return <NewTabLink href={props.explorer.addressUrl(addr)}>{addr}</NewTabLink>;
} else {
return <React.Fragment>{addr}</React.Fragment>;
}
};
export default Address;
export default connect((state: AppState) => ({
toChecksumAddress: getChecksumAddressFn(state)
}))(Address);

View File

@ -0,0 +1,24 @@
.Identicon {
position: relative;
width: 4rem;
height: 4rem;
&-img {
display: block;
height: 100%;
width: 100%;
padding: 0px;
border-radius: 50%;
}
&-shadow {
position: absolute;
height: 100%;
width: 100%;
top: 0;
border-radius: 50%;
pointer-events: none;
box-shadow: 0 1px 2px rgba(#000, 0.15),
0 0 3px rgba(#000, 0.15) inset;
}
}

View File

@ -1,49 +1,43 @@
import { isValidETHAddress } from 'libs/validators';
import React from 'react';
import makeBlockie from 'ethereum-blockies-base64';
import { connect } from 'react-redux';
import { getIsValidAddressFn } from 'selectors/config';
import { AppState } from 'reducers';
import './Identicon.scss';
interface Props {
interface OwnProps {
address: string;
className?: string;
size?: string;
}
export default function Identicon(props: Props) {
interface StateProps {
isValidAddress: ReturnType<typeof getIsValidAddressFn>;
}
type Props = OwnProps & StateProps;
const Identicon: React.SFC<Props> = props => {
const size = props.size || '4rem';
const { address, className = '' } = props;
const identiconDataUrl = isValidETHAddress(address) ? makeBlockie(address) : '';
const { address, isValidAddress, className = '' } = props;
const identiconDataUrl = isValidAddress(address) ? makeBlockie(address) : '';
return (
// Use inline styles for printable wallets
<div
className={`Identicon ${className}`}
title="Address Identicon"
style={{ width: size, height: size, position: 'relative' }}
style={{ width: size, height: size }}
aria-hidden={!identiconDataUrl}
>
{identiconDataUrl && (
<img
src={identiconDataUrl}
alt="Unique Address Image"
style={{
height: '100%',
width: '100%',
padding: '0px',
borderRadius: '50%'
}}
/>
<img className="Identicon-img" src={identiconDataUrl} alt="Unique Address Image" />
)}
<div
className="border"
style={{
position: 'absolute',
height: '100%',
width: '100%',
top: 0,
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.15), inset 0 0 3px 0 rgba(0, 0, 0, 0.15)',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
<div className="Identicon-shadow" />
</div>
);
}
};
export default connect((state: AppState): StateProps => ({
isValidAddress: getIsValidAddressFn(state)
}))(Identicon);

View File

@ -1 +1,7 @@
[]
[
{
"name": "Bridge",
"address": "0x0000000000000000000000000000000001000006",
"abi": "[{ \"name\": \"getFederationAddress\", \"type\": \"function\", \"constant\": true, \"inputs\": [], \"outputs\": [{ \"name\": \"\", \"type\": \"string\" }] }]"
}
]

View File

@ -78,6 +78,11 @@ export const ETH_SINGULAR: DPath = {
value: "m/0'/0'/0'"
};
export const RSK_TESTNET: DPath = {
label: 'Testnet (RSK)',
value: "m/44'/37310'/0'/0"
};
export const GO_DEFAULT: DPath = {
label: 'Default (GO)',
value: "m/44'/6060'/0'/0"
@ -99,6 +104,7 @@ export const DPaths: DPath[] = [
ETSC_DEFAULT,
EGEM_DEFAULT,
CLO_DEFAULT,
RSK_TESTNET,
GO_DEFAULT
];

View File

@ -3,8 +3,9 @@ import { connect } from 'react-redux';
import Select from 'react-select';
import moment from 'moment';
import translate from 'translations';
import { isValidTxHash, isValidETHAddress } from 'libs/validators';
import { isValidTxHash } from 'libs/validators';
import { getRecentNetworkTransactions } from 'selectors/transactions';
import { getIsValidAddressFn } from 'selectors/config';
import { AppState } from 'reducers';
import { Input } from 'components/ui';
import './TxHashInput.scss';
@ -15,6 +16,7 @@ interface OwnProps {
}
interface ReduxProps {
recentTxs: AppState['transactions']['recent'];
isValidAddress: ReturnType<typeof getIsValidAddressFn>;
}
type Props = OwnProps & ReduxProps;
@ -40,7 +42,7 @@ class TxHashInput extends React.Component<Props, State> {
}
public render() {
const { recentTxs } = this.props;
const { recentTxs, isValidAddress } = this.props;
const { hash } = this.state;
let selectOptions: Option[] = [];
@ -81,7 +83,7 @@ class TxHashInput extends React.Component<Props, State> {
onChange={this.handleChange}
/>
{isValidETHAddress(hash) && (
{isValidAddress(hash) && (
<p className="TxHashInput-message help-block is-invalid">
{translate('SELECT_RECENT_TX_BY_TXHASH')}
</p>
@ -116,5 +118,6 @@ class TxHashInput extends React.Component<Props, State> {
}
export default connect((state: AppState): ReduxProps => ({
recentTxs: getRecentNetworkTransactions(state)
recentTxs: getRecentNetworkTransactions(state),
isValidAddress: getIsValidAddressFn(state)
}))(TxHashInput);

View File

@ -1,9 +1,9 @@
import React, { Component } from 'react';
import translate, { translateRaw } from 'translations';
import { getNetworkContracts } from 'selectors/config';
import { getNetworkContracts, getIsValidAddressFn } from 'selectors/config';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { isValidETHAddress, isValidAbiJson } from 'libs/validators';
import { isValidAbiJson } from 'libs/validators';
import { NetworkContract } from 'types/network';
import { donationAddressMap } from 'config';
import { Input, TextArea, CodeBlock, Dropdown } from 'components/ui';
@ -20,6 +20,7 @@ interface ContractOption {
interface StateProps {
currentTo: ReturnType<typeof getCurrentTo>;
contracts: NetworkContract[];
isValidAddress: ReturnType<typeof getIsValidAddressFn>;
}
interface OwnProps {
@ -75,9 +76,9 @@ class InteractForm extends Component<Props, State> {
};
public render() {
const { contracts, accessContract, currentTo } = this.props;
const { contracts, accessContract, currentTo, isValidAddress } = this.props;
const { abiJson, contract } = this.state;
const validEthAddress = isValidETHAddress(
const validEthAddress = isValidAddress(
currentTo.value ? addHexPrefix(currentTo.value.toString('hex')) : ''
);
const validAbiJson = isValidAbiJson(abiJson);
@ -203,7 +204,8 @@ class InteractForm extends Component<Props, State> {
const mapStateToProps = (state: AppState) => ({
contracts: getNetworkContracts(state) || [],
currentTo: getCurrentTo(state)
currentTo: getCurrentTo(state),
isValidAddress: getIsValidAddressFn(state)
});
export default connect(mapStateToProps, { setCurrentTo })(InteractForm);

View File

@ -18,6 +18,7 @@ interface State {
passwordValidation: ZXCVBNResult | null;
feedback: string;
}
export default class EnterPassword extends Component<Props, State> {
public state: State = {
password: '',

View File

@ -7,6 +7,7 @@ import DownloadWallet from './DownloadWallet';
import EnterPassword from './EnterPassword';
import PaperWallet from './PaperWallet';
import FinalSteps from '../FinalSteps';
import { N_FACTOR } from 'config';
export enum Steps {
Password = 'password',
@ -87,7 +88,7 @@ export default class GenerateKeystore extends Component<{}, State> {
private generateWalletAndContinue = (password: string) => {
this.setState({ isGenerating: true });
generateKeystore(password).then(res => {
generateKeystore(password, N_FACTOR).then(res => {
this.setState({
password,
activeStep: Steps.Download,

View File

@ -31,7 +31,7 @@ const PaperWallet: React.SFC<Props> = props => (
</label>
{/* Download Paper Wallet */}
<h2 className="GenPaper-title">{translate('X_PRINT')}</h2>
<h2 className="GenPaper-title">{translate('X_SAVE_PAPER')}</h2>
<div className="GenPaper-paper">
<PrintableWallet address={props.keystore.address} privateKey={props.privateKey} />
</div>

View File

@ -1,43 +1,63 @@
import React from 'react';
import { toChecksumAddress } from 'ethereumjs-util';
import translate, { translateRaw } from 'translations';
import { IWallet } from 'libs/wallet';
import { print } from 'components/PrintableWallet';
import { QRCode } from 'components/ui';
import { GenerateKeystoreModal, TogglablePassword, AddressField } from 'components';
import { QRCode, Modal } from 'components/ui';
import {
GenerateKeystoreModal,
TogglablePassword,
AddressField,
PrintableWallet
} from 'components';
import './WalletInfo.scss';
import { getChecksumAddressFn } from 'selectors/config';
import { AppState } from 'reducers';
import { connect } from 'react-redux';
interface Props {
interface OwnProps {
wallet: IWallet;
}
interface StateProps {
toChecksumAddress: ReturnType<typeof getChecksumAddressFn>;
}
type Props = OwnProps & StateProps;
interface State {
address: string;
privateKey: string;
isPrivateKeyVisible: boolean;
isKeystoreModalOpen: boolean;
isPaperWalletModalOpen: boolean;
}
export default class WalletInfo extends React.PureComponent<Props, State> {
class WalletInfo extends React.PureComponent<Props, State> {
public state = {
address: '',
privateKey: '',
isPrivateKeyVisible: false,
isKeystoreModalOpen: false
isKeystoreModalOpen: false,
isPaperWalletModalOpen: false
};
public componentDidMount() {
this.setStateFromWallet(this.props.wallet);
this.setStateFromWallet(this.props);
}
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (this.props.wallet !== nextProps.wallet) {
this.setStateFromWallet(nextProps.wallet);
this.setStateFromWallet(nextProps);
}
}
public render() {
const { address, privateKey, isPrivateKeyVisible, isKeystoreModalOpen } = this.state;
const {
address,
privateKey,
isPrivateKeyVisible,
isKeystoreModalOpen,
isPaperWalletModalOpen
} = this.state;
return (
<div className="WalletInfo">
@ -85,11 +105,11 @@ export default class WalletInfo extends React.PureComponent<Props, State> {
<div className="col-xs-6">
<label>{translate('WALLET_INFO_UTILITIES')}</label>
<button className="btn btn-info btn-block" onClick={print(address, privateKey)}>
{translate('X_PRINT')}
<button className="btn btn-info btn-block" onClick={this.openPaperWalletModal}>
{translate('X_SAVE_PAPER')}
</button>
<button className="btn btn-info btn-block" onClick={this.toggleKeystoreModal}>
<button className="btn btn-info btn-block" onClick={this.openKeystoreModal}>
{translate('GENERATE_KEYSTORE_TITLE')}
</button>
</div>
@ -99,15 +119,20 @@ export default class WalletInfo extends React.PureComponent<Props, State> {
<GenerateKeystoreModal
isOpen={isKeystoreModalOpen}
privateKey={privateKey}
handleClose={this.toggleKeystoreModal}
handleClose={this.closeKeystoreModal}
/>
<Modal isOpen={isPaperWalletModalOpen} handleClose={this.closePaperWalletModal}>
<PrintableWallet address={address} privateKey={privateKey} />
</Modal>
</div>
</div>
</div>
);
}
private setStateFromWallet(wallet: IWallet) {
private setStateFromWallet(props: Props) {
const { wallet, toChecksumAddress } = props;
const address = toChecksumAddress(wallet.getAddressString());
const privateKey = wallet.getPrivateKeyString ? wallet.getPrivateKeyString() : '';
this.setState({ address, privateKey });
@ -117,7 +142,13 @@ export default class WalletInfo extends React.PureComponent<Props, State> {
this.setState({ isPrivateKeyVisible: !this.state.isPrivateKeyVisible });
};
private toggleKeystoreModal = () => {
this.setState({ isKeystoreModalOpen: !this.state.isKeystoreModalOpen });
};
private openKeystoreModal = () => this.setState({ isKeystoreModalOpen: true });
private closeKeystoreModal = () => this.setState({ isKeystoreModalOpen: false });
private openPaperWalletModal = () => this.setState({ isPaperWalletModalOpen: true });
private closePaperWalletModal = () => this.setState({ isPaperWalletModalOpen: false });
}
export default connect((state: AppState): StateProps => ({
toChecksumAddress: getChecksumAddressFn(state)
}))(WalletInfo);

View File

@ -103,6 +103,9 @@ class FieldsClass extends Component<Props> {
}
export const Fields = connect(
(state: AppState) => ({ unit: getUnit(state), currentBalance: getCurrentBalance(state) }),
(state: AppState) => ({
unit: getUnit(state),
currentBalance: getCurrentBalance(state)
}),
{ resetWallet }
)(FieldsClass);

View File

@ -1,6 +1,7 @@
import abi from 'ethereumjs-abi';
import { toChecksumAddress, addHexPrefix, stripHexPrefix } from 'ethereumjs-util';
import { addHexPrefix, stripHexPrefix } from 'ethereumjs-util';
import BN from 'bn.js';
import { toChecksumAddressByChainId } from 'utils/formatters';
import {
FuncParams,
FunctionOutputMappings,
@ -33,7 +34,7 @@ export default class AbiFunction {
return encodedCall;
};
public decodeInput = (argString: string) => {
public decodeInput = (argString: string, chainId: number) => {
// Remove method selector from data, if present
argString = argString.replace(addHexPrefix(this.methodSelector), '');
// Convert argdata to a hex buffer for ethereumjs-abi
@ -46,12 +47,12 @@ export default class AbiFunction {
const currType = this.inputTypes[index];
return {
...argObj,
[currName]: this.parsePostDecodedValue(currType, currArg)
[currName]: this.parsePostDecodedValue(currType, currArg, chainId)
};
}, {});
};
public decodeOutput = (argString: string) => {
public decodeOutput = (argString: string, chainId: number) => {
// Remove method selector from data, if present
argString = argString.replace(addHexPrefix(this.methodSelector), '');
@ -69,7 +70,7 @@ export default class AbiFunction {
const currType = this.outputTypes[index];
return {
...argObj,
[currName]: this.parsePostDecodedValue(currType, currArg)
[currName]: this.parsePostDecodedValue(currType, currArg, chainId)
};
}, {});
};
@ -85,9 +86,9 @@ export default class AbiFunction {
this.methodSelector = abi.methodID(this.name, this.inputTypes).toString('hex');
}
private parsePostDecodedValue = (type: string, value: any) => {
private parsePostDecodedValue = (type: string, value: any, chainId: number) => {
const valueMapping: ITypeMapping = {
address: (val: any) => toChecksumAddress(val.toString(16))
address: (val: any) => toChecksumAddressByChainId(val.toString(16), chainId)
};
const mapppedType = valueMapping[type];

View File

@ -104,6 +104,7 @@ export const NODE_CONFIGS: { [key in StaticNetworkIds]: RawNodeConfig[] } = {
url: 'https://node.expanse.tech/'
}
],
POA: [
{
name: makeNodeName('POA', 'core'),
@ -167,6 +168,15 @@ export const NODE_CONFIGS: { [key in StaticNetworkIds]: RawNodeConfig[] } = {
}
],
RSK_TESTNET: [
{
name: makeNodeName('RSK_TESTNET', 'rsk_testnet'),
type: 'rpc',
service: 'mycrypto.testnet.rsk.co',
url: 'https://mycrypto.testnet.rsk.co/'
}
],
GO: [
{
name: makeNodeName('GO', 'go'),

View File

@ -1,7 +1,7 @@
import Tx from 'ethereumjs-tx';
import { bufferToHex } from 'ethereumjs-util';
import { Wei } from 'libs/units';
import { isValidETHAddress } from 'libs/validators';
import { isValidAddress } from 'libs/validators';
import { IFullWallet } from 'libs/wallet';
import { translateRaw } from 'translations';
import { ITransaction, IHexStrTransaction } from '../typings';
@ -69,7 +69,7 @@ const gasParamsInRange = (t: ITransaction) => {
};
const validAddress = (t: ITransaction) => {
if (!isValidETHAddress(bufferToHex(t.to))) {
if (!isValidAddress(bufferToHex(t.to), t.chainId)) {
throw Error(translateRaw('ERROR_5'));
}
};

View File

@ -1,4 +1,5 @@
import { toChecksumAddress, isValidPrivate } from 'ethereumjs-util';
import { isValidChecksumAddress as isValidChecksumRSKAddress } from 'rskjs-util';
import { stripHexPrefix } from 'libs/values';
import WalletAddressValidator from 'wallet-address-validator';
import { normalise } from './ens';
@ -16,8 +17,18 @@ import { dPathRegex, ETC_LEDGER, ETH_SINGULAR } from 'config/dpaths';
import { EAC_SCHEDULING_CONFIG } from './scheduling';
import BN from 'bn.js';
// FIXME we probably want to do checksum checks sideways
export function isValidETHAddress(address: string): boolean {
export function getIsValidAddressFunction(chainId: number) {
if (chainId === 30 || chainId === 31) {
return (address: string) => isValidRSKAddress(address, chainId);
}
return isValidETHAddress;
}
export function isValidAddress(address: string, chainId: number) {
return getIsValidAddressFunction(chainId)(address);
}
function isValidETHLikeAddress(address: string, extraChecks?: () => boolean): boolean {
if (address === '0x0000000000000000000000000000000000000000') {
return false;
}
@ -28,10 +39,18 @@ export function isValidETHAddress(address: string): boolean {
} else if (/^(0x)?[0-9a-f]{40}$/.test(address) || /^(0x)?[0-9A-F]{40}$/.test(address)) {
return true;
} else {
return isChecksumAddress(address);
return extraChecks ? extraChecks() : false;
}
}
export function isValidETHAddress(address: string): boolean {
return isValidETHLikeAddress(address, () => isChecksumAddress(address));
}
export function isValidRSKAddress(address: string, chainId: number): boolean {
return isValidETHLikeAddress(address, () => isValidChecksumRSKAddress(address, chainId));
}
export const isCreationAddress = (address: string): boolean =>
address === '0x0' || address === '0x0000000000000000000000000000000000000000';
@ -339,15 +358,16 @@ export function isValidAddressLabel(
address: string,
label: string,
addresses: { [address: string]: string },
labels: { [label: string]: string }
labels: { [label: string]: string },
chainId: number
) {
const addressAlreadyExists = !!addresses[address];
const addressAlreadyExists = !!addresses[address.toLowerCase()];
const labelAlreadyExists = !!labels[label];
const result: { isValid: boolean; addressError?: string; labelError?: string } = {
isValid: true
};
if (!isValidETHAddress(address)) {
if (!isValidAddress(address, chainId)) {
result.addressError = translateRaw('INVALID_ADDRESS');
}

View File

@ -1,6 +1,6 @@
import { getTransactionFields, makeTransaction } from 'libs/transaction';
import { IFullWallet } from '../IWallet';
import { bufferToHex, toChecksumAddress } from 'ethereumjs-util';
import { bufferToHex } from 'ethereumjs-util';
import { configuredStore } from 'store';
import { getNodeLib, getNetworkByChainId } from 'selectors/config';
import Web3Node from 'libs/nodes/web3';
@ -16,7 +16,7 @@ export default class Web3Wallet implements IFullWallet {
}
public getAddressString(): string {
return toChecksumAddress(this.address);
return this.address;
}
public signRawTransaction(): Promise<Buffer> {

View File

@ -1,5 +1,4 @@
import { IV3Wallet } from 'ethereumjs-wallet';
import { N_FACTOR } from 'config';
import Worker from 'worker-loader!./workers/generateKeystore.worker.ts';
interface KeystorePayload {
@ -8,7 +7,10 @@ interface KeystorePayload {
privateKey: string;
}
export default function generateKeystore(password: string): Promise<KeystorePayload> {
export default function generateKeystore(
password: string,
N_FACTOR: number
): Promise<KeystorePayload> {
return new Promise(resolve => {
const worker = new Worker();
worker.postMessage({ password, N_FACTOR });

View File

@ -1,4 +1,3 @@
import { toChecksumAddress } from 'ethereumjs-util';
import { TypeKeys, AddressBookAction, AddressLabelEntry } from 'actions/addressBook';
export interface State {
@ -23,15 +22,16 @@ export function addressBook(state: State = INITIAL_STATE, action: AddressBookAct
switch (action.type) {
case TypeKeys.SET_ADDRESS_LABEL: {
const { addresses, labels } = state;
const { address, label } = action.payload;
const checksummedAddress = toChecksumAddress(address);
const { label } = action.payload;
const address = action.payload.address.toLowerCase();
const updatedAddresses = {
...addresses,
[checksummedAddress]: label
[address]: label
};
const updatedLabels = {
...labels,
[label]: checksummedAddress
[label]: address
};
return {
@ -43,12 +43,12 @@ export function addressBook(state: State = INITIAL_STATE, action: AddressBookAct
case TypeKeys.CLEAR_ADDRESS_LABEL: {
const { addresses, labels } = state;
const address = action.payload;
const address = action.payload.toLowerCase();
const label = addresses[address];
const updatedAddresses = { ...addresses };
const updatedLabels = { ...labels };
delete updatedAddresses[toChecksumAddress(address)];
delete updatedAddresses[address];
delete updatedLabels[label];
return {
@ -59,9 +59,8 @@ export function addressBook(state: State = INITIAL_STATE, action: AddressBookAct
}
case TypeKeys.SET_ADDRESS_LABEL_ENTRY: {
const { id, address } = action.payload;
const checksummedAddress = toChecksumAddress(address);
const isNonRowEntry = id === 'ADDRESS_BOOK_TABLE_ID' || id === 'ACCOUNT_ADDRESS_ID';
const { id } = action.payload;
const address = action.payload.address.toLowerCase();
return {
...state,
@ -69,7 +68,7 @@ export function addressBook(state: State = INITIAL_STATE, action: AddressBookAct
...state.entries,
[id]: {
...action.payload,
address: isNonRowEntry ? address : checksummedAddress
address
}
}
};

View File

@ -21,6 +21,7 @@ import {
ETSC_DEFAULT,
EGEM_DEFAULT,
CLO_DEFAULT,
RSK_TESTNET,
GO_DEFAULT
} from 'config/dpaths';
import { ConfigAction } from 'actions/config';
@ -367,6 +368,32 @@ export const INITIAL_STATE: State = {
}
},
RSK_TESTNET: {
id: 'RSK_TESTNET',
name: 'RSK',
unit: 'SBTC',
chainId: 31,
color: '#58A052',
isCustom: false,
blockExplorer: makeExplorer({
name: 'RSK Testnet Explorer',
origin: 'https://explorer.testnet.rsk.co'
}),
tokens: require('config/tokens/rsk.json'),
contracts: require('config/contracts/rsk.json'),
isTestnet: true,
dPathFormats: {
[SecureWalletName.TREZOR]: RSK_TESTNET,
[SecureWalletName.LEDGER_NANO_S]: RSK_TESTNET,
[InsecureWalletName.MNEMONIC_PHRASE]: RSK_TESTNET
},
gasPriceSettings: {
min: 0.183,
max: 1.5,
initial: 0.183
}
},
GO: {
id: 'GO',
name: 'GO',

View File

@ -21,6 +21,7 @@ import {
getNextAddressLabelId
} from 'selectors/addressBook';
import { showNotification } from 'actions/notifications';
import { getNetworkChainId } from 'selectors/config';
export const ERROR_DURATION: number = 4000;
@ -32,15 +33,21 @@ export function* handleChangeAddressLabelEntry(action: ChangeAddressLabelEntry):
isEditing,
overrideValidation
} = action.payload;
const addresses = yield select(getAddressLabels);
const labels = yield select(getLabelAddresses);
const priorEntry = yield select(getAddressLabelEntry, id);
const addresses: ReturnType<typeof getAddressLabels> = yield select(getAddressLabels);
const labels: ReturnType<typeof getLabelAddresses> = yield select(getLabelAddresses);
const priorEntry: ReturnType<typeof getAddressLabelEntry> = yield select(
getAddressLabelEntry,
id
);
const chainId: ReturnType<typeof getNetworkChainId> = yield select(getNetworkChainId);
const { addressError, labelError } = isValidAddressLabel(
temporaryAddress,
temporaryLabel,
addresses,
labels
labels,
chainId
);
const updatedEntry = {
id,
address: addressError && !isEditing ? priorEntry.address || '' : temporaryAddress,
@ -56,8 +63,13 @@ export function* handleChangeAddressLabelEntry(action: ChangeAddressLabelEntry):
export function* handleSaveAddressLabelEntry(action: SaveAddressLabelEntry): SagaIterator {
const id = action.payload;
const { address, addressError, label, labelError } = yield select(getAddressLabelEntry, id);
const nextId = yield select(getNextAddressLabelId);
const {
address,
addressError,
label,
labelError
}: ReturnType<typeof getAddressLabelEntry> = yield select(getAddressLabelEntry, id);
const nextId: ReturnType<typeof getNextAddressLabelId> = yield select(getNextAddressLabelId);
const flashError = (error: string) => put(showNotification('danger', error, ERROR_DURATION));
if (addressError) {
@ -101,7 +113,10 @@ export function* handleSaveAddressLabelEntry(action: SaveAddressLabelEntry): Sag
})
);
} else if (id === ACCOUNT_ADDRESS_ID) {
const ownEntry = yield select(getAddressLabelEntryFromAddress, address);
const ownEntry: ReturnType<typeof getAddressLabelEntryFromAddress> = yield select(
getAddressLabelEntryFromAddress,
address
);
yield put(
setAddressLabelEntry({
@ -143,7 +158,10 @@ export function* handleSaveAddressLabelEntry(action: SaveAddressLabelEntry): Sag
export function* handleRemoveAddressLabelEntry(action: RemoveAddressLabelEntry): SagaIterator {
const id = action.payload;
const { id: entryId, address } = yield select(getAddressLabelEntry, id);
const { id: entryId, address }: ReturnType<typeof getAddressLabelEntry> = yield select(
getAddressLabelEntry,
id
);
if (typeof entryId === 'undefined') {
return;
@ -153,7 +171,10 @@ export function* handleRemoveAddressLabelEntry(action: RemoveAddressLabelEntry):
yield put(clearAddressLabelEntry(id));
if (id === ACCOUNT_ADDRESS_ID) {
const ownEntry = yield select(getAddressLabelEntryFromAddress, address);
const ownEntry: ReturnType<typeof getAddressLabelEntryFromAddress> = yield select(
getAddressLabelEntryFromAddress,
address
);
if (ownEntry) {
yield put(clearAddressLabelEntry(ownEntry.id));

View File

@ -5,12 +5,12 @@ import {
updateDeterministicWallet
} from 'actions/deterministicWallets';
import { showNotification } from 'actions/notifications';
import { publicToAddress, toChecksumAddress } from 'ethereumjs-util';
import { publicToAddress } from 'ethereumjs-util';
import HDKey from 'hdkey';
import { INode } from 'libs/nodes/INode';
import { SagaIterator } from 'redux-saga';
import { all, apply, fork, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
import { getNodeLib } from 'selectors/config';
import { getNodeLib, getChecksumAddressFn } from 'selectors/config';
import { getDesiredToken, getWallets } from 'selectors/deterministicWallets';
import { getTokens } from 'selectors/wallet';
import translate from 'translations';
@ -37,6 +37,9 @@ export function* getDeterministicWallets(action: GetDeterministicWalletsAction):
return;
}
const wallets: DeterministicWalletData[] = [];
const toChecksumAddress: ReturnType<typeof getChecksumAddressFn> = yield select(
getChecksumAddressFn
);
for (let i = 0; i < limit; i++) {
const index = i + offset;
const dkey = hdk.derive(`${pathBase}/${index}`);

View File

@ -5,14 +5,16 @@ import { setTokenTo } from 'actions/transaction/actionCreators/meta';
import { Address } from 'libs/units';
import { select, call, put, takeLatest, take } from 'redux-saga/effects';
import { SagaIterator } from 'redux-saga';
import { isValidENSAddress, isValidETHAddress } from 'libs/validators';
import { isValidENSAddress } from 'libs/validators';
import { TypeKeys } from 'actions/transaction/constants';
import { getResolvedAddress } from 'selectors/ens';
import { resolveDomainRequested, TypeKeys as ENSTypekeys } from 'actions/ens';
import { SetToFieldAction, SetTokenToMetaAction } from 'actions/transaction';
import { getIsValidAddressFn } from 'selectors/config';
export function* setCurrentTo({ payload: raw }: SetCurrentToAction): SagaIterator {
const validAddress: boolean = yield call(isValidETHAddress, raw);
const isValidAddress: ReturnType<typeof getIsValidAddressFn> = yield select(getIsValidAddressFn);
const validAddress: boolean = yield call(isValidAddress, raw);
const validEns: boolean = yield call(isValidENSAddress, raw);
let value: Buffer | null = null;

View File

@ -1,7 +1,6 @@
import { SagaIterator } from 'redux-saga';
import { put, select, apply, call, take, takeEvery } from 'redux-saga/effects';
import EthTx from 'ethereumjs-tx';
import { toChecksumAddress } from 'ethereumjs-util';
import {
setTransactionData,
FetchTransactionDataAction,
@ -15,7 +14,7 @@ import {
BroadcastTransactionSucceededAction,
BroadcastTransactionFailedAction
} from 'actions/transaction';
import { getNodeLib, getNetworkConfig } from 'selectors/config';
import { getNodeLib, getNetworkConfig, getChecksumAddressFn } from 'selectors/config';
import { getWalletInst } from 'selectors/wallet';
import { INode } from 'libs/nodes';
import { hexEncodeData } from 'libs/nodes/rpc/utils';
@ -99,6 +98,7 @@ export function* getSaveableTransaction(tx: EthTx, hash: string): SagaIterator {
}
}
const toChecksumAddress = yield select(getChecksumAddressFn);
const savableTx: SavedTransaction = {
hash,
from,

View File

@ -1,8 +1,6 @@
import { toChecksumAddress } from 'ethereumjs-util';
import { AppState } from 'reducers';
import { ADDRESS_BOOK_TABLE_ID } from 'components/AddressBookTable';
import { ACCOUNT_ADDRESS_ID } from 'components/BalanceSidebar/AccountAddress';
import { AddressLabelEntry } from 'actions/addressBook';
import { getCurrentTo } from './transaction';
export function getAddressLabels(state: AppState) {
@ -31,10 +29,7 @@ export function getAccountAddressEntry(state: AppState) {
export function getAddressLabelEntryFromAddress(state: AppState, address: string) {
const rows = getAddressLabelRows(state);
const entry = rows.find(
(iteratedEntry: AddressLabelEntry) => iteratedEntry.address === toChecksumAddress(address)
);
const entry = rows.find(e => e.address === address.toLowerCase());
return entry;
}
@ -63,5 +58,5 @@ export function getCurrentToLabel(state: AppState) {
const addresses = getAddressLabels(state);
const currentTo = getCurrentTo(state);
return addresses[currentTo.raw] || null;
return addresses[currentTo.raw.toLowerCase()] || null;
}

View File

@ -7,6 +7,8 @@ import {
} from 'types/network';
import { getNodeConfig } from 'selectors/config';
import { stripWeb3Network } from 'libs/nodes';
import { getIsValidAddressFunction } from 'libs/validators';
import { getChecksumAddressFunction } from 'utils/formatters';
const getConfig = (state: AppState) => state.config;
export const getNetworks = (state: AppState) => getConfig(state).networks;
@ -90,3 +92,17 @@ export const getAllNetworkConfigs = (state: AppState) => ({
export const isNetworkUnit = (state: AppState, unit: string) => {
return unit === getNetworkUnit(state);
};
export const getNetworkChainId = (state: AppState) => {
return getNetworkConfig(state).chainId;
};
export const getIsValidAddressFn = (state: AppState) => {
const chainId = getNetworkChainId(state);
return getIsValidAddressFunction(chainId);
};
export const getChecksumAddressFn = (state: AppState) => {
const chainId = getNetworkChainId(state);
return getChecksumAddressFunction(chainId);
};

View File

@ -47,6 +47,7 @@ export function isNetworkUnit(state: AppState, unit: string) {
export function isWalletFormatSupportedOnNetwork(state: AppState, format: WalletName): boolean {
const network = getStaticNetworkConfig(state);
const chainId = network ? network.chainId : 0;
const CHECK_FORMATS: DPathFormat[] = [
SecureWalletName.LEDGER_NANO_S,
@ -65,6 +66,11 @@ export function isWalletFormatSupportedOnNetwork(state: AppState, format: Wallet
return !!dPath;
}
// Parity signer on RSK
if (chainId === 30 || (chainId === 31 && format === SecureWalletName.PARITY_SIGNER)) {
return false;
}
// All other wallet formats are supported
return true;
}

View File

@ -6,10 +6,9 @@ import { getDecimalFromEtherUnit } from 'libs/units';
import { getSerializedTransaction } from 'selectors/transaction';
import EthTx from 'ethereumjs-tx';
import { getCustomTokens } from 'selectors/customTokens';
import { getNetworkConfig } from 'selectors/config';
import { getNetworkConfig, getChecksumAddressFn } from 'selectors/config';
import { Token } from '../../../shared/types/network';
import { stripHexPrefixAndLower } from 'libs/values';
import { toChecksumAddress } from 'ethereumjs-util';
const getMetaState = (state: AppState) => getTransactionState(state).meta;
const getFrom = (state: AppState) => {
@ -20,6 +19,7 @@ const getFrom = (state: AppState) => {
try {
const from = transactionInstance.from;
if (from) {
const toChecksumAddress = getChecksumAddressFn(state);
return toChecksumAddress(from.toString('hex'));
}
} catch (e) {
@ -52,6 +52,7 @@ const getUnit = (state: AppState) => {
networkTokens = networkConfig.tokens;
}
const mergedTokens = networkTokens ? [...networkTokens, ...customTokens] : customTokens;
const toChecksumAddress = getChecksumAddressFn(state);
const stringTo = toChecksumAddress(stripHexPrefixAndLower(to.toString('hex')));
const result = mergedTokens.find(t => t.address === stringTo);
if (result) {

View File

@ -0,0 +1,47 @@
import { State as AddressBookState, INITIAL_STATE } from 'reducers/addressBook';
import { ADDRESS_BOOK_TABLE_ID } from 'components/AddressBookTable';
import { ACCOUNT_ADDRESS_ID } from 'components/BalanceSidebar/AccountAddress';
export default function rehydrateAddressBookState(
ab: AddressBookState | undefined
): AddressBookState {
const addressBook = { ...INITIAL_STATE };
if (!ab) {
return INITIAL_STATE;
}
// Lower case addresses
Object.keys(ab.addresses).forEach(address => {
addressBook.addresses[address.toLowerCase()] = ab.addresses[address];
});
Object.keys(ab.labels).forEach(label => {
addressBook.labels[label] = ab.labels[label].toLowerCase();
});
Object.keys(ab.entries).forEach(id => {
// Remove the temporary entries that address book and account store info in
if (id === ADDRESS_BOOK_TABLE_ID || id === ACCOUNT_ADDRESS_ID) {
return;
}
const entry = {
...ab.entries[id],
address: ab.entries[id].address.toLowerCase()
};
// Convert errorous entries into temporary ones
if (entry.addressError) {
entry.temporaryAddress = entry.address;
delete entry.addressError;
}
if (entry.labelError) {
entry.temporaryLabel = entry.label;
delete entry.labelError;
}
addressBook.entries[id] = entry;
});
return addressBook;
}

View File

@ -10,10 +10,7 @@ import {
} from 'reducers/transactions';
import { State as SwapState, INITIAL_STATE as initialSwapState } from 'reducers/swap';
import { State as WalletState, INITIAL_STATE as initialWalletState } from 'reducers/wallet';
import {
State as AddressBookState,
INITIAL_STATE as initialAddressBookState
} from 'reducers/addressBook';
import { State as AddressBookState } from 'reducers/addressBook';
import { applyMiddleware, createStore, Store } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import { createLogger } from 'redux-logger';
@ -26,7 +23,7 @@ import {
rehydrateConfigAndCustomTokenState,
getConfigAndCustomTokensStateToSubscribe
} from './configAndTokens';
import fixAddressBookErrors from 'utils/fixAddressBookErrors';
import rehydrateAddressBookState from './addressBook';
const configureStore = () => {
const logger = createLogger({
@ -78,10 +75,7 @@ const configureStore = () => {
...initialTransactionsState,
...savedTransactionsState
},
addressBook: {
...initialAddressBookState,
...fixAddressBookErrors(savedAddressBook)
},
addressBook: rehydrateAddressBookState(savedAddressBook),
wallet: {
...initialWalletState,
...savedWalletState

View File

@ -50,6 +50,7 @@
"X_MIST": "Mist",
"X_WEB3": "Web3",
"X_MNEMONIC": "Mnemonic Phrase ",
"X_SAVE_PAPER": "Save Paper Wallet ",
"X_HARDWARE_WALLET": "hardware wallet ",
"X_PRINT": "Print Paper Wallet ",
"X_PRINTDESC": "ProTip: Click print and save this as a PDF, even if you do not own a printer! ",
@ -124,7 +125,6 @@
"GEN_SUCCESSMSG": "Success! Your wallet has been generated. ",
"GEN_LABEL_2": "Save your `Keystore` File. ",
"GEN_LABEL_3": "Save Your Address. ",
"GEN_LABEL_4": "Print paper wallet or a QR code. ",
"GEN_ARIA_1": "Enter a strong password (at least 9 characters)",
"GEN_ARIA_2": "make password visible",
"BULK_LABEL_1": "Number of Wallets To Generate ",

19
common/typescript/rskjs-util.d.ts vendored Normal file
View File

@ -0,0 +1,19 @@
declare module 'rskjs-util' {
/**
*
* @description Returns a checksummed address
* @export
* @param {string} address
* @returns {string}
*/
export function toChecksumAddress(address: string, chainId: number): string;
/**
*
* @description Checks if the address is a valid checksummed address
* @export
* @param {string} address
* @returns {boolean}
*/
export function isValidChecksumAddress(address: string, chainId: number): boolean;
}

View File

@ -1,28 +0,0 @@
import { State as AddressBookState } from 'reducers/addressBook';
import { ADDRESS_BOOK_TABLE_ID } from 'components/AddressBookTable';
import { ACCOUNT_ADDRESS_ID } from 'components/BalanceSidebar/AccountAddress';
export default function fixAddressBookErrors(addressBook: AddressBookState | undefined) {
if (!addressBook) {
return {};
}
Object.keys(addressBook.entries).forEach(entryId => {
const entry = addressBook.entries[entryId];
if (entry.addressError) {
entry.temporaryAddress = entry.address;
delete entry.addressError;
}
if (entry.labelError) {
entry.temporaryLabel = entry.label;
delete entry.labelError;
}
});
delete addressBook.entries[ADDRESS_BOOK_TABLE_ID];
delete addressBook.entries[ACCOUNT_ADDRESS_ID];
return addressBook;
}

View File

@ -1,4 +1,6 @@
import BN from 'bn.js';
import { toChecksumAddress as toETHChecksumAddress } from 'ethereumjs-util';
import { toChecksumAddress as toRSKChecksumAddress } from 'rskjs-util';
import { Wei } from 'libs/units';
import { stripHexPrefix } from 'libs/values';
@ -119,3 +121,15 @@ export function ensV3Url(name: string) {
export function hexToNumber(hex: string) {
return new BN(stripHexPrefix(hex)).toNumber();
}
// Checksumming split into two functions so it's shared by network selector
export function getChecksumAddressFunction(chainId: number) {
if (chainId === 30 || chainId === 31) {
return (addr: string) => toRSKChecksumAddress(addr, chainId);
}
return toETHChecksumAddress;
}
export function toChecksumAddressByChainId(address: string, chainId: number) {
return getChecksumAddressFunction(chainId)(address);
}

View File

@ -1,52 +0,0 @@
import React from 'react';
import { renderToString } from 'react-dom/server';
interface PrintOptions {
styles?: string;
printTimeout?: number;
popupFeatures?: object;
}
export default function(element: React.ReactElement<any>, opts: PrintOptions = {}) {
const options = {
styles: '',
printTimeout: 500,
popupFeatures: {},
...opts
};
// Convert popupFeatures into a key=value,key=value string. See
// https://developer.mozilla.org/en-US/docs/Web/API/Window/open#Window_features
// for more information.
const featuresStr = Object.entries(options.popupFeatures)
.map(([key, value]) => `${key}=${value}`)
.join(',');
const popup = window.open('about:blank', 'printWindow', featuresStr);
if (popup) {
popup.document.open();
popup.document.write(`
<html>
<head>
<style>${options.styles}</style>
<script>
setTimeout(function() {
window.print();
}, ${options.printTimeout});
</script>
</head>
<body>
${renderToString(element)}
<script>
// FIXME consider if we REALLY want it
var width = document.body.children[0].offsetWidth;
var height = document.body.children[0].offsetHeight;
window.moveTo(0, 0);
// FIXME Chrome could be larger (i guess?)
window.resizeTo(width + 60, height + 60);
</script>
</body>
</html>
`);
}
}

View File

@ -28,6 +28,7 @@
"ethereumjs-wallet": "0.6.0",
"font-awesome": "4.7.0",
"hdkey": "0.8.0",
"html2canvas": "1.0.0-alpha.12",
"idna-uts46": "1.1.0",
"jsonschema": "1.2.4",
"lodash": "4.17.5",
@ -55,6 +56,7 @@
"redux": "3.7.2",
"redux-logger": "3.0.6",
"redux-saga": "0.16.0",
"rskjs-util": "1.0.3",
"scryptsy": "2.0.0",
"semver": "5.5.0",
"uuid": "3.2.1",
@ -69,6 +71,7 @@
"@types/enzyme-adapter-react-16": "1.0.1",
"@types/events": "1.2.0",
"@types/history": "4.6.2",
"@types/html2canvas": "0.0.33",
"@types/jest": "22.2.3",
"@types/lodash": "4.14.107",
"@types/moment-timezone": "0.5.4",

View File

@ -15,6 +15,7 @@ type StaticNetworkIds =
| 'ETSC'
| 'EGEM'
| 'CLO'
| 'RSK_TESTNET'
| 'GO';
export interface BlockExplorerConfig {

View File

@ -5,20 +5,24 @@ import {
isValidPath,
isValidPrivKey,
isLabelWithoutENS,
isValidAddressLabel
isValidAddressLabel,
isValidAddress
} from '../../common/libs/validators';
import { translateRaw } from '../../common/translations';
import { DPaths } from 'config/dpaths';
import { valid, invalid } from '../utils/testStrings';
configuredStore.getState();
const VALID_BTC_ADDRESS = '1MEWT2SGbqtz6mPCgFcnea8XmWV5Z4Wc6';
const VALID_ETH_ADDRESS = '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8';
const VALID_RSK_TESTNET_ADDRESS = '0x5aAeb6053F3e94c9b9A09F33669435E7EF1BEaEd';
const VALID_ETH_PRIVATE_KEY = '3f4fd89ea4970cc77bfd2d07a95786575ea62e183857afe6301578e1a3c5c782';
const INVALID_ETH_PRIVATE_KEY = '3f4fd89ea4970cc77bfd2d07a95786575ea62e183857afe6301578e1a3c5ZZZZ';
const VALID_ETH_PRIVATE_BUFFER = Buffer.from(VALID_ETH_PRIVATE_KEY, 'hex');
const VALID_ETH_PRIVATE_0X = '0x3f4fd89ea4970cc77bfd2d07a95786575ea62e183857afe6301578e1a3c5c782';
const RSK_TESTNET_CHAIN_ID = 31;
const RSK_MAINNET_CHAIN_ID = 30;
const ETH_CHAIN_ID = 1;
describe('Validator', () => {
it('should validate correct BTC address as true', () => {
@ -34,6 +38,18 @@ describe('Validator', () => {
it('should validate incorrect ETH address as false', () => {
expect(isValidETHAddress('nonsense' + VALID_ETH_ADDRESS + 'nonsense')).toBeFalsy();
});
it('should validate correct ETH address in RSK network as false', () => {
expect(isValidAddress(VALID_ETH_ADDRESS, RSK_TESTNET_CHAIN_ID)).toBeFalsy();
});
it('should validate correct RSK address in ETH network as false', () => {
expect(isValidAddress(VALID_RSK_TESTNET_ADDRESS, ETH_CHAIN_ID)).toBeFalsy();
});
it('should validate correct RSK address in RSK testnet network as true', () => {
expect(isValidAddress(VALID_RSK_TESTNET_ADDRESS, RSK_TESTNET_CHAIN_ID)).toBeTruthy();
});
it('should validate correct RSK address in RSK mainnet network as false', () => {
expect(isValidAddress(VALID_RSK_TESTNET_ADDRESS, RSK_MAINNET_CHAIN_ID)).toBeFalsy();
});
it('should validate an incorrect DPath as false', () => {
expect(isValidPath('m/44/60/0/0')).toBeFalsy();
});
@ -92,33 +108,39 @@ describe('isValidAddressLabel', () => {
describe('Happy path', () => {
it('should return valid', () => {
expect(isValidAddressLabel(validAddress, 'Foo', {}, {}).isValid).toEqual(true);
expect(isValidAddressLabel(validAddress, 'Foo', {}, {}, 1).isValid).toEqual(true);
});
});
describe('Invalid cases', () => {
it('should return invalid when the provided address is invalid', () => {
const { isValid, addressError } = isValidAddressLabel('derp', 'Foo', {}, {});
const { isValid, addressError } = isValidAddressLabel('derp', 'Foo', {}, {}, 1);
expect(isValid).toEqual(false);
expect(addressError).toEqual(translateRaw('INVALID_ADDRESS'));
});
it('should return invalid if the address already exists', () => {
const { isValid, addressError } = isValidAddressLabel(validAddress, 'Foo', addresses, labels);
const { isValid, addressError } = isValidAddressLabel(
validAddress,
'Foo',
addresses,
labels,
1
);
expect(isValid).toEqual(false);
expect(addressError).toEqual(translateRaw('ADDRESS_ALREADY_EXISTS'));
});
it('should return invalid if the label is not of correct length', () => {
const { isValid, labelError } = isValidAddressLabel(validAddress, 'X', {}, {});
const { isValid, labelError } = isValidAddressLabel(validAddress, 'X', {}, {}, 1);
expect(isValid).toEqual(false);
expect(labelError).toEqual(translateRaw('INVALID_LABEL_LENGTH'));
});
it('should return invalid if the label contains an ENS TLD', () => {
const { isValid, labelError } = isValidAddressLabel(validAddress, 'Foo.eth', {}, {});
const { isValid, labelError } = isValidAddressLabel(validAddress, 'Foo.eth', {}, {}, 1);
expect(isValid).toEqual(false);
expect(labelError).toEqual(translateRaw('LABEL_CANNOT_CONTAIN_ENS_SUFFIX'));
@ -129,7 +151,8 @@ describe('isValidAddressLabel', () => {
otherValidAddress,
'Foo',
addresses,
labels
labels,
1
);
expect(isValid).toEqual(false);

View File

@ -13,7 +13,8 @@ import {
changeAddressLabelEntry,
saveAddressLabelEntry,
clearAddressLabelEntry,
removeAddressLabelEntry
removeAddressLabelEntry,
AddressLabel
} from 'actions/addressBook';
import { getInitialState } from '../selectors/helpers';
@ -295,11 +296,11 @@ describe('addressBook: Sagas', () => {
}
};
const action = removeAddressLabelEntry(id);
const dispatched: string[] = [];
const dispatched: AddressLabel[] = [];
await runSaga(
{
dispatch: (dispatching: string) => dispatched.push(dispatching),
dispatch: (dispatching: AddressLabel) => dispatched.push(dispatching),
getState: () => state
},
handleRemoveAddressLabelEntry,

View File

@ -5,7 +5,8 @@ import { all, apply, fork, put, select } from 'redux-saga/effects';
import RpcNode from 'libs/nodes/rpc';
import { getDesiredToken, getWallets } from 'selectors/deterministicWallets';
import { getTokens } from 'selectors/wallet';
import { getNodeLib } from 'selectors/config';
import { getNodeLib, getChecksumAddressFn } from 'selectors/config';
import { getChecksumAddressFunction } from 'utils/formatters';
import * as dWalletActions from 'actions/deterministicWallets';
import {
getDeterministicWallets,
@ -45,6 +46,8 @@ const genWalletData2 = () => ({
const genBalances = () => [Wei('100'), Wei('200')];
describe('getDeterministicWallets*', () => {
const toChecksumAddress = getChecksumAddressFunction(1);
describe('starting from seed', () => {
const dWallet = {
seed:
@ -54,8 +57,12 @@ describe('getDeterministicWallets*', () => {
const action = dWalletActions.getDeterministicWallets(dWallet);
const gen = getDeterministicWallets(action);
it('should select getChecksumAddressFn', () => {
expect(gen.next().value).toEqual(select(getChecksumAddressFn));
});
it('should match put snapshot', () => {
expect(gen.next().value).toMatchSnapshot();
expect(gen.next(toChecksumAddress).value).toMatchSnapshot();
});
it('should fork updateWalletValues', () => {
@ -79,8 +86,12 @@ describe('getDeterministicWallets*', () => {
const action = dWalletActions.getDeterministicWallets(dWallet);
const gen = getDeterministicWallets(action);
it('should select getChecksumAddressFn', () => {
expect(gen.next().value).toEqual(select(getChecksumAddressFn));
});
it('should match put snapshot', () => {
expect(gen.next().value).toMatchSnapshot();
expect(gen.next(toChecksumAddress).value).toMatchSnapshot();
});
it('should fork updateWalletValues', () => {

View File

@ -1,12 +1,15 @@
import { getResolvedAddress } from 'selectors/ens';
import { Address } from 'libs/units';
import { call, select, put, take } from 'redux-saga/effects';
import { isValidETHAddress, isValidENSAddress } from 'libs/validators';
import { isValidENSAddress, getIsValidAddressFunction } from 'libs/validators';
import { setCurrentTo, setField } from 'sagas/transaction/current/currentTo';
import { isEtherTransaction } from 'selectors/transaction';
import { cloneableGenerator } from 'redux-saga/utils';
import { setToField, setTokenTo } from 'actions/transaction';
import { resolveDomainRequested, TypeKeys as ENSTypekeys } from 'actions/ens';
import { getIsValidAddressFn } from 'selectors/config';
const isValidAddress = getIsValidAddressFunction(1);
describe('setCurrentTo*', () => {
const data = {} as any;
@ -22,8 +25,13 @@ describe('setCurrentTo*', () => {
};
data.validEthGen = setCurrentTo(ethAddrAction);
it('should call isValidETHAddress', () => {
expect(data.validEthGen.next().value).toEqual(call(isValidETHAddress, raw));
it('should select getIsValidAddressFn', () => {
expect(data.validEthGen.next().value).toEqual(select(getIsValidAddressFn));
});
it('should call isValidAddress', () => {
expect(data.validEthGen.next(isValidAddress).value).toEqual(call(isValidAddress, raw));
});
it('should call isValidENSAddress', () => {
@ -48,8 +56,12 @@ describe('setCurrentTo*', () => {
};
data.validEnsGen = setCurrentTo(ensAddrAction);
it('should call isValidETHAddress', () => {
expect(data.validEnsGen.next().value).toEqual(call(isValidETHAddress, raw));
it('should select getIsValidAddressFn', () => {
expect(data.validEnsGen.next().value).toEqual(select(getIsValidAddressFn));
});
it('should call isValidAddress', () => {
expect(data.validEnsGen.next(isValidAddress).value).toEqual(call(isValidAddress, raw));
});
it('should call isValidENSAddress', () => {

View File

@ -126,10 +126,20 @@
version "4.6.2"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0"
"@types/html2canvas@0.0.33":
version "0.0.33"
resolved "https://registry.yarnpkg.com/@types/html2canvas/-/html2canvas-0.0.33.tgz#e4076f0ea664d3cd6e1460c82e48b177774052fe"
dependencies:
"@types/jquery" "*"
"@types/jest@22.2.3", "@types/jest@^22.2.2":
version "22.2.3"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-22.2.3.tgz#0157c0316dc3722c43a7b71de3fdf3acbccef10d"
"@types/jquery@*":
version "3.3.2"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.2.tgz#8700226bdde24b6f98e3a60126dbaab3b2a3ab41"
"@types/lodash@4.14.107":
version "4.14.107"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.107.tgz#b2d2ae3958bfb8ff828495cbe12214af9e4d035e"
@ -1360,6 +1370,10 @@ base62@^1.1.0:
version "1.2.8"
resolved "https://registry.yarnpkg.com/base62/-/base62-1.2.8.tgz#1264cb0fb848d875792877479dbe8bae6bae3428"
base64-arraybuffer@^0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
base64-js@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978"
@ -2788,6 +2802,12 @@ css-color-names@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
css-line-break@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-1.0.1.tgz#19f2063a33e95fb2831b86446c0b80c188af450a"
dependencies:
base64-arraybuffer "^0.1.5"
css-loader@0.28.11:
version "0.28.11"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.11.tgz#c3f9864a700be2711bb5a2462b2389b1a392dab7"
@ -5351,6 +5371,12 @@ html-webpack-plugin@3.0.6:
toposort "^1.0.0"
util.promisify "1.0.0"
html2canvas@1.0.0-alpha.12:
version "1.0.0-alpha.12"
resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.0.0-alpha.12.tgz#3b1992e3c9b3f56063c35fd620494f37eba88513"
dependencies:
css-line-break "1.0.1"
htmlparser2@^3.9.1:
version "3.9.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
@ -10013,6 +10039,12 @@ rn-host-detect@^1.0.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/rn-host-detect/-/rn-host-detect-1.1.3.tgz#242d76e2fa485c48d751416e65b7cce596969e91"
rskjs-util@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/rskjs-util/-/rskjs-util-1.0.3.tgz#c0242d9308d7dc1eb653ffbf0dd1af1309bf55ca"
dependencies:
keccak "^1.0.2"
rst-selector-parser@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"