diff --git a/common/actions/addressBook/actionCreators.ts b/common/actions/addressBook/actionCreators.ts index 72682775..f772ff29 100644 --- a/common/actions/addressBook/actionCreators.ts +++ b/common/actions/addressBook/actionCreators.ts @@ -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 diff --git a/common/components/AddressBookTable.tsx b/common/components/AddressBookTable.tsx index c01c5f51..fbbffd48 100644 --- a/common/components/AddressBookTable.tsx +++ b/common/components/AddressBookTable.tsx @@ -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; addressLabels: ReturnType; labelAddresses: ReturnType; + toChecksumAddress: ReturnType; } type Props = DispatchProps & StateProps; @@ -200,7 +202,9 @@ class AddressBookTable extends React.Component { 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 = state => ({ rows: getAddressLabelRows(state), entry: getAddressBookTableEntry(state), addressLabels: getAddressLabels(state), - labelAddresses: getLabelAddresses(state) + labelAddresses: getLabelAddresses(state), + toChecksumAddress: getChecksumAddressFn(state) }); const mapDispatchToProps: DispatchProps = { diff --git a/common/components/AddressField.tsx b/common/components/AddressField.tsx index 28029d4e..7a084d7c 100644 --- a/common/components/AddressField.tsx +++ b/common/components/AddressField.tsx @@ -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 = ({ +interface StateProps { + toChecksumAddress: ReturnType; +} + +type Props = OwnProps & StateProps; + +const AddressField: React.SFC = ({ isReadOnly, isSelfAddress, isCheckSummed, - showLabelMatch + showLabelMatch, + toChecksumAddress }) => ( = ({ )} /> ); + +export default connect((state: AppState): StateProps => ({ + toChecksumAddress: getChecksumAddressFn(state) +}))(AddressField); diff --git a/common/components/BalanceSidebar/AccountAddress.tsx b/common/components/BalanceSidebar/AccountAddress.tsx index 497dfc15..4fd5ea71 100644 --- a/common/components/BalanceSidebar/AccountAddress.tsx +++ b/common/components/BalanceSidebar/AccountAddress.tsx @@ -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; - addressLabels: ReturnType; + addressLabel: string; } interface DispatchProps { @@ -65,13 +65,12 @@ class AccountAddress extends React.Component { } 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 { 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 { { ); } else { - labelContent = ( - - ); + labelContent = ; } return labelContent; }; private generateLabelButton = () => { - const { address, addressLabels } = this.props; + const { addressLabel } = this.props; const { editingLabel } = this.state; - const label = addressLabels[address]; const labelButton = editingLabel ? ( @@ -175,10 +168,10 @@ class AccountAddress extends React.Component { - {label ? translate('EDIT_LABEL') : translate('ADD_LABEL_9')} + {addressLabel ? translate('EDIT_LABEL') : translate('ADD_LABEL_9')} ); @@ -187,13 +180,12 @@ class AccountAddress extends React.Component { }; 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 { private clearTemporaryLabelTouched = () => this.setState({ labelInputTouched: false }); } -const mapStateToProps: MapStateToProps = (state: AppState) => ({ - entry: getAccountAddressEntry(state), - addressLabels: getAddressLabels(state) -}); +const mapStateToProps: MapStateToProps = ( + state: AppState, + ownProps: OwnProps +) => { + const labelEntry = getAddressLabelEntryFromAddress(state, ownProps.address); + return { + entry: getAccountAddressEntry(state), + addressLabel: labelEntry ? labelEntry.label : '' + }; +}; const mapDispatchToProps: DispatchProps = { changeAddressLabelEntry, diff --git a/common/components/BalanceSidebar/AccountInfo.tsx b/common/components/BalanceSidebar/AccountInfo.tsx index 3d9e23fe..030b0217 100644 --- a/common/components/BalanceSidebar/AccountInfo.tsx +++ b/common/components/BalanceSidebar/AccountInfo.tsx @@ -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; isOffline: ReturnType; + toChecksumAddress: ReturnType; } interface State { @@ -73,7 +73,7 @@ class AccountInfo extends React.Component { }; 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 }; diff --git a/common/components/ConfirmationModal/components/Body/components/Addresses.tsx b/common/components/ConfirmationModal/components/Body/components/Addresses.tsx index f69f821b..dd0a5570 100644 --- a/common/components/ConfirmationModal/components/Body/components/Addresses.tsx +++ b/common/components/ConfirmationModal/components/Body/components/Addresses.tsx @@ -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; + unit: ReturnType; isToken: boolean; + toChecksumAddress: ReturnType; } const size = '3rem'; class AddressesClass extends Component { public render() { - const { from, isToken, unit } = this.props; - + const { from, isToken, unit, toChecksumAddress } = this.props; return ( { @@ -92,7 +92,8 @@ class AddressesClass extends Component { 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); diff --git a/common/components/PaperWallet/index.scss b/common/components/PaperWallet/index.scss new file mode 100644 index 00000000..57f93fa4 --- /dev/null +++ b/common/components/PaperWallet/index.scss @@ -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; + } + } +} diff --git a/common/components/PaperWallet/index.tsx b/common/components/PaperWallet/index.tsx index 1d4c7b85..1127161d 100644 --- a/common/components/PaperWallet/index.tsx +++ b/common/components/PaperWallet/index.tsx @@ -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 { + private container: HTMLElement | null; + public render() { const { privateKey } = this.props; const address = toChecksumAddress(addHexPrefix(this.props.address)); return ( -
- MyCrypto Logo - ETH Logo +
(this.container = el)}> + MyCrypto Logo -
-
+
+
-

YOUR ADDRESS

+

YOUR ADDRESS

-
- -

AMOUNT / NOTES

+
+ +

AMOUNT / NOTES

-
-
+
+
-

YOUR PRIVATE KEY

+

YOUR PRIVATE KEY

-
-

- Your Address: +

+

+ Your Address:
{address}

-

- Your Private Key: +

+ Your Private Key:
{privateKey}

-
-
+
+
-

Always look for this icon when sending to this wallet

+

+ Always look for this icon when sending to this wallet +

); } + + public toPNG = async () => { + if (!this.container) { + return ''; + } + const canvas = await html2canvas(this.container); + return canvas.toDataURL('image/png'); + }; } diff --git a/common/components/PrintableWallet/index.tsx b/common/components/PrintableWallet/index.tsx index 3800b5b3..47ad54c0 100644 --- a/common/components/PrintableWallet/index.tsx +++ b/common/components/PrintableWallet/index.tsx @@ -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(, { - 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 = ({ address, privateKey }) => { - const pkey = stripHexPrefix(privateKey); +interface State { + paperWalletImage: string; +} - return ( - - ); -}; +export default class PrintableWallet extends React.Component { + 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 ( +
+ (this.paperWallet = c)} /> + + {translate('X_SAVE_PAPER')} + +
+ ); + } +} diff --git a/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx b/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx index 67b046ce..f94eb71e 100644 --- a/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx +++ b/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx @@ -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 { 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 { 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 diff --git a/common/components/WalletDecrypt/components/ViewOnly.tsx b/common/components/WalletDecrypt/components/ViewOnly.tsx index 02e1dec5..fc7b5267 100644 --- a/common/components/WalletDecrypt/components/ViewOnly.tsx +++ b/common/components/WalletDecrypt/components/ViewOnly.tsx @@ -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; + isValidAddress: ReturnType; } type Props = OwnProps & StateProps; @@ -29,9 +30,9 @@ class ViewOnlyDecryptClass extends PureComponent { }; 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 { 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 { } export const ViewOnlyDecrypt = connect((state: AppState): StateProps => ({ - recentAddresses: getRecentAddresses(state) + recentAddresses: getRecentAddresses(state), + isValidAddress: getIsValidAddressFn(state) }))(ViewOnlyDecryptClass); diff --git a/common/components/index.ts b/common/components/index.ts index df33259b..013e6645 100644 --- a/common/components/index.ts +++ b/common/components/index.ts @@ -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'; diff --git a/common/components/ui/Address.tsx b/common/components/ui/Address.tsx index 209c8452..49e56db7 100644 --- a/common/components/ui/Address.tsx +++ b/common/components/ui/Address.tsx @@ -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; } -interface WalletProps extends BaseProps { - wallet: IWallet; -} +type Props = BaseProps & StateProps; -type Props = AddressProps | WalletProps; +export class Address extends React.PureComponent { + 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 => { - let addr = ''; - if (isAddressProps(props)) { - addr = props.address; - } else { - addr = props.wallet.getAddressString(); + if (explorer) { + return {renderAddress}; + } else { + return {renderAddress}; + } } - addr = toChecksumAddress(addr); +} - if (props.explorer) { - return {addr}; - } else { - return {addr}; - } -}; - -export default Address; +export default connect((state: AppState) => ({ + toChecksumAddress: getChecksumAddressFn(state) +}))(Address); diff --git a/common/components/ui/Identicon.scss b/common/components/ui/Identicon.scss new file mode 100644 index 00000000..428fc51f --- /dev/null +++ b/common/components/ui/Identicon.scss @@ -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; + } +} diff --git a/common/components/ui/Identicon.tsx b/common/components/ui/Identicon.tsx index fc4de891..f01d6d94 100644 --- a/common/components/ui/Identicon.tsx +++ b/common/components/ui/Identicon.tsx @@ -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; +} + +type Props = OwnProps & StateProps; + +const Identicon: React.SFC = 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
{identiconDataUrl && ( - Unique Address Image + Unique Address Image )} -
+
); -} +}; + +export default connect((state: AppState): StateProps => ({ + isValidAddress: getIsValidAddressFn(state) +}))(Identicon); diff --git a/common/config/contracts/rsk.json b/common/config/contracts/rsk.json index fe51488c..c7025d99 100644 --- a/common/config/contracts/rsk.json +++ b/common/config/contracts/rsk.json @@ -1 +1,7 @@ -[] +[ + { + "name": "Bridge", + "address": "0x0000000000000000000000000000000001000006", + "abi": "[{ \"name\": \"getFederationAddress\", \"type\": \"function\", \"constant\": true, \"inputs\": [], \"outputs\": [{ \"name\": \"\", \"type\": \"string\" }] }]" + } +] diff --git a/common/config/dpaths.ts b/common/config/dpaths.ts index fd043b20..90b594dd 100644 --- a/common/config/dpaths.ts +++ b/common/config/dpaths.ts @@ -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 ]; diff --git a/common/containers/Tabs/CheckTransaction/components/TxHashInput.tsx b/common/containers/Tabs/CheckTransaction/components/TxHashInput.tsx index 1342c325..5f072462 100644 --- a/common/containers/Tabs/CheckTransaction/components/TxHashInput.tsx +++ b/common/containers/Tabs/CheckTransaction/components/TxHashInput.tsx @@ -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; } type Props = OwnProps & ReduxProps; @@ -40,7 +42,7 @@ class TxHashInput extends React.Component { } 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 { onChange={this.handleChange} /> - {isValidETHAddress(hash) && ( + {isValidAddress(hash) && (

{translate('SELECT_RECENT_TX_BY_TXHASH')}

@@ -116,5 +118,6 @@ class TxHashInput extends React.Component { } export default connect((state: AppState): ReduxProps => ({ - recentTxs: getRecentNetworkTransactions(state) + recentTxs: getRecentNetworkTransactions(state), + isValidAddress: getIsValidAddressFn(state) }))(TxHashInput); diff --git a/common/containers/Tabs/Contracts/components/Interact/components/InteractForm/index.tsx b/common/containers/Tabs/Contracts/components/Interact/components/InteractForm/index.tsx index b8972aa1..68a71d37 100644 --- a/common/containers/Tabs/Contracts/components/Interact/components/InteractForm/index.tsx +++ b/common/containers/Tabs/Contracts/components/Interact/components/InteractForm/index.tsx @@ -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; contracts: NetworkContract[]; + isValidAddress: ReturnType; } interface OwnProps { @@ -75,9 +76,9 @@ class InteractForm extends Component { }; 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 { const mapStateToProps = (state: AppState) => ({ contracts: getNetworkContracts(state) || [], - currentTo: getCurrentTo(state) + currentTo: getCurrentTo(state), + isValidAddress: getIsValidAddressFn(state) }); export default connect(mapStateToProps, { setCurrentTo })(InteractForm); diff --git a/common/containers/Tabs/GenerateWallet/components/Keystore/EnterPassword.tsx b/common/containers/Tabs/GenerateWallet/components/Keystore/EnterPassword.tsx index 6ceffd67..fd985a4c 100644 --- a/common/containers/Tabs/GenerateWallet/components/Keystore/EnterPassword.tsx +++ b/common/containers/Tabs/GenerateWallet/components/Keystore/EnterPassword.tsx @@ -18,6 +18,7 @@ interface State { passwordValidation: ZXCVBNResult | null; feedback: string; } + export default class EnterPassword extends Component { public state: State = { password: '', diff --git a/common/containers/Tabs/GenerateWallet/components/Keystore/Keystore.tsx b/common/containers/Tabs/GenerateWallet/components/Keystore/Keystore.tsx index 8bfb7016..f9838893 100644 --- a/common/containers/Tabs/GenerateWallet/components/Keystore/Keystore.tsx +++ b/common/containers/Tabs/GenerateWallet/components/Keystore/Keystore.tsx @@ -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, diff --git a/common/containers/Tabs/GenerateWallet/components/Keystore/PaperWallet.tsx b/common/containers/Tabs/GenerateWallet/components/Keystore/PaperWallet.tsx index 445fab2d..572786da 100644 --- a/common/containers/Tabs/GenerateWallet/components/Keystore/PaperWallet.tsx +++ b/common/containers/Tabs/GenerateWallet/components/Keystore/PaperWallet.tsx @@ -31,7 +31,7 @@ const PaperWallet: React.SFC = props => ( {/* Download Paper Wallet */} -

{translate('X_PRINT')}

+

{translate('X_SAVE_PAPER')}

diff --git a/common/containers/Tabs/SendTransaction/components/WalletInfo.tsx b/common/containers/Tabs/SendTransaction/components/WalletInfo.tsx index 00e79251..e116940d 100644 --- a/common/containers/Tabs/SendTransaction/components/WalletInfo.tsx +++ b/common/containers/Tabs/SendTransaction/components/WalletInfo.tsx @@ -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; +} + +type Props = OwnProps & StateProps; + interface State { address: string; privateKey: string; isPrivateKeyVisible: boolean; isKeystoreModalOpen: boolean; + isPaperWalletModalOpen: boolean; } -export default class WalletInfo extends React.PureComponent { +class WalletInfo extends React.PureComponent { 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 (
@@ -85,11 +105,11 @@ export default class WalletInfo extends React.PureComponent {
- -
@@ -99,15 +119,20 @@ export default class WalletInfo extends React.PureComponent { + + + +
); } - 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 { 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); diff --git a/common/containers/Tabs/Swap/components/LiteSend/Fields.tsx b/common/containers/Tabs/Swap/components/LiteSend/Fields.tsx index 79eee65f..91764841 100644 --- a/common/containers/Tabs/Swap/components/LiteSend/Fields.tsx +++ b/common/containers/Tabs/Swap/components/LiteSend/Fields.tsx @@ -103,6 +103,9 @@ class FieldsClass extends Component { } export const Fields = connect( - (state: AppState) => ({ unit: getUnit(state), currentBalance: getCurrentBalance(state) }), + (state: AppState) => ({ + unit: getUnit(state), + currentBalance: getCurrentBalance(state) + }), { resetWallet } )(FieldsClass); diff --git a/common/libs/contracts/ABIFunction.ts b/common/libs/contracts/ABIFunction.ts index 3240e652..b2c7ab15 100644 --- a/common/libs/contracts/ABIFunction.ts +++ b/common/libs/contracts/ABIFunction.ts @@ -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]; diff --git a/common/libs/nodes/configs.ts b/common/libs/nodes/configs.ts index ed78b26e..bff01e96 100644 --- a/common/libs/nodes/configs.ts +++ b/common/libs/nodes/configs.ts @@ -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'), diff --git a/common/libs/transaction/utils/ether.ts b/common/libs/transaction/utils/ether.ts index 031d6927..528ca0ed 100644 --- a/common/libs/transaction/utils/ether.ts +++ b/common/libs/transaction/utils/ether.ts @@ -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')); } }; diff --git a/common/libs/validators.ts b/common/libs/validators.ts index a72feb25..b6be4d8a 100644 --- a/common/libs/validators.ts +++ b/common/libs/validators.ts @@ -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'); } diff --git a/common/libs/wallet/non-deterministic/web3.ts b/common/libs/wallet/non-deterministic/web3.ts index 8a834361..03065a5c 100644 --- a/common/libs/wallet/non-deterministic/web3.ts +++ b/common/libs/wallet/non-deterministic/web3.ts @@ -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 { diff --git a/common/libs/web-workers/generateKeystore.ts b/common/libs/web-workers/generateKeystore.ts index 87072c99..a36861c2 100644 --- a/common/libs/web-workers/generateKeystore.ts +++ b/common/libs/web-workers/generateKeystore.ts @@ -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 { +export default function generateKeystore( + password: string, + N_FACTOR: number +): Promise { return new Promise(resolve => { const worker = new Worker(); worker.postMessage({ password, N_FACTOR }); diff --git a/common/reducers/addressBook.ts b/common/reducers/addressBook.ts index 233c2dfd..f025eb47 100644 --- a/common/reducers/addressBook.ts +++ b/common/reducers/addressBook.ts @@ -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 } } }; diff --git a/common/reducers/config/networks/staticNetworks.ts b/common/reducers/config/networks/staticNetworks.ts index 2be72a85..de165099 100644 --- a/common/reducers/config/networks/staticNetworks.ts +++ b/common/reducers/config/networks/staticNetworks.ts @@ -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', diff --git a/common/sagas/addressBook.ts b/common/sagas/addressBook.ts index 0fbf72a8..f8299639 100644 --- a/common/sagas/addressBook.ts +++ b/common/sagas/addressBook.ts @@ -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 = yield select(getAddressLabels); + const labels: ReturnType = yield select(getLabelAddresses); + const priorEntry: ReturnType = yield select( + getAddressLabelEntry, + id + ); + const chainId: ReturnType = 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 = yield select(getAddressLabelEntry, id); + const nextId: ReturnType = 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 = 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 = 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 = yield select( + getAddressLabelEntryFromAddress, + address + ); if (ownEntry) { yield put(clearAddressLabelEntry(ownEntry.id)); diff --git a/common/sagas/deterministicWallets.ts b/common/sagas/deterministicWallets.ts index da15d31b..9bc6ce1b 100644 --- a/common/sagas/deterministicWallets.ts +++ b/common/sagas/deterministicWallets.ts @@ -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 = yield select( + getChecksumAddressFn + ); for (let i = 0; i < limit; i++) { const index = i + offset; const dkey = hdk.derive(`${pathBase}/${index}`); diff --git a/common/sagas/transaction/current/currentTo.ts b/common/sagas/transaction/current/currentTo.ts index 4717c141..d51a1820 100644 --- a/common/sagas/transaction/current/currentTo.ts +++ b/common/sagas/transaction/current/currentTo.ts @@ -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 = yield select(getIsValidAddressFn); + const validAddress: boolean = yield call(isValidAddress, raw); const validEns: boolean = yield call(isValidENSAddress, raw); let value: Buffer | null = null; diff --git a/common/sagas/transactions.ts b/common/sagas/transactions.ts index 566d569b..9174dc25 100644 --- a/common/sagas/transactions.ts +++ b/common/sagas/transactions.ts @@ -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, diff --git a/common/selectors/addressBook.ts b/common/selectors/addressBook.ts index 77462b9d..3c771da6 100644 --- a/common/selectors/addressBook.ts +++ b/common/selectors/addressBook.ts @@ -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; } diff --git a/common/selectors/config/networks.ts b/common/selectors/config/networks.ts index fcb5a82d..f2d5c928 100644 --- a/common/selectors/config/networks.ts +++ b/common/selectors/config/networks.ts @@ -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); +}; diff --git a/common/selectors/config/wallet.ts b/common/selectors/config/wallet.ts index f944dbdb..3e354724 100644 --- a/common/selectors/config/wallet.ts +++ b/common/selectors/config/wallet.ts @@ -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; } diff --git a/common/selectors/transaction/meta.ts b/common/selectors/transaction/meta.ts index 8e90ad96..7877bfeb 100644 --- a/common/selectors/transaction/meta.ts +++ b/common/selectors/transaction/meta.ts @@ -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) { diff --git a/common/store/addressBook.ts b/common/store/addressBook.ts new file mode 100644 index 00000000..3db29b5d --- /dev/null +++ b/common/store/addressBook.ts @@ -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; +} diff --git a/common/store/store.ts b/common/store/store.ts index 76f060cf..9ca45c23 100644 --- a/common/store/store.ts +++ b/common/store/store.ts @@ -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 diff --git a/common/translations/lang/en.json b/common/translations/lang/en.json index bc337ef2..a32cdc39 100644 --- a/common/translations/lang/en.json +++ b/common/translations/lang/en.json @@ -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 ", diff --git a/common/typescript/rskjs-util.d.ts b/common/typescript/rskjs-util.d.ts new file mode 100644 index 00000000..73d224e7 --- /dev/null +++ b/common/typescript/rskjs-util.d.ts @@ -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; +} diff --git a/common/utils/fixAddressBookErrors.ts b/common/utils/fixAddressBookErrors.ts deleted file mode 100644 index a6506756..00000000 --- a/common/utils/fixAddressBookErrors.ts +++ /dev/null @@ -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; -} diff --git a/common/utils/formatters.ts b/common/utils/formatters.ts index 7df7f3cf..383117aa 100644 --- a/common/utils/formatters.ts +++ b/common/utils/formatters.ts @@ -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); +} diff --git a/common/utils/printElement.ts b/common/utils/printElement.ts deleted file mode 100644 index bf3db1ab..00000000 --- a/common/utils/printElement.ts +++ /dev/null @@ -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, 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(` - - - - - - - ${renderToString(element)} - - - - `); - } -} diff --git a/package.json b/package.json index c6e69ca9..0a021cb4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/shared/types/network.d.ts b/shared/types/network.d.ts index 03fd4fb9..ca63fd84 100644 --- a/shared/types/network.d.ts +++ b/shared/types/network.d.ts @@ -15,6 +15,7 @@ type StaticNetworkIds = | 'ETSC' | 'EGEM' | 'CLO' + | 'RSK_TESTNET' | 'GO'; export interface BlockExplorerConfig { diff --git a/spec/libs/validators.spec.ts b/spec/libs/validators.spec.ts index 2ea81599..d1ab7dab 100644 --- a/spec/libs/validators.spec.ts +++ b/spec/libs/validators.spec.ts @@ -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); diff --git a/spec/sagas/addressBook.spec.ts b/spec/sagas/addressBook.spec.ts index d153b7bc..1297ab36 100644 --- a/spec/sagas/addressBook.spec.ts +++ b/spec/sagas/addressBook.spec.ts @@ -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, diff --git a/spec/sagas/deterministicWallets.spec.ts b/spec/sagas/deterministicWallets.spec.ts index f05cf444..57e73ce3 100644 --- a/spec/sagas/deterministicWallets.spec.ts +++ b/spec/sagas/deterministicWallets.spec.ts @@ -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', () => { diff --git a/spec/sagas/transaction/current/currentTo.spec.ts b/spec/sagas/transaction/current/currentTo.spec.ts index 08ce23aa..cd3886aa 100644 --- a/spec/sagas/transaction/current/currentTo.spec.ts +++ b/spec/sagas/transaction/current/currentTo.spec.ts @@ -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', () => { diff --git a/yarn.lock b/yarn.lock index db14bd44..9e2dcfe1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"