Support Non-Ethereum Networks (#849)

* Make UnlockHeader a PureComponent

* MVP

* actually disable wallet format if not determined to be valid format for wallet

* default to correct derivation in mnemonic modal

* cleanup

* fix tslint

* use enums for HD wallet getPath

* Add stricter typing

* Fix labels not updating on selector

* Ban hardware wallet support for custom network unsupported chainIds

* Fix type error

* Fix custom node dPath not being saved

* Fix mnemonic modal

* default path bugfixes

* add react-select

* misc fixes; rabbit holing hard.

* fix tslint

* revert identicon changes

* reload on network change :/

* actually reload on network change

* really really reload on network change

* tslint fixes

* Update styles

* set table width

* fix package versioning

* push broken sagas

* Fix saga test

* fix tslint

* address round of review

* move non-selectors out to utilty; adjust reload timer

* cleanup network util comments

* manage wallet disable at WalletDecrypt instead of in both WalletDecrypt and WalletButton

* Separate WalletDecrypt props into ownProps / StateProps

* disable payment requests on non-eth networks

* specialize connect; separate props

* remove unused state prop

* remove bad import

* create tests for networks

* Clarify Lite-Send error on non-ethereum networkS

* remove string option for network config name

* Create concept of always-on 'EXTRA_PATHS'; include SINGULAR_DTV legacy dPath in 'EXTRA_PATHS'

* fix multiple imports

* address PR comments
This commit is contained in:
Daniel Ternyak 2018-01-20 14:06:28 -06:00 committed by GitHub
parent 2420f5488b
commit ab5fa1a799
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 957 additions and 501 deletions

View File

@ -1,6 +1,6 @@
import * as interfaces from './actionTypes';
import { TypeKeys } from './constants';
import { NodeConfig, CustomNodeConfig, NetworkConfig, CustomNetworkConfig } from 'config/data';
import { NodeConfig, CustomNodeConfig, NetworkConfig, CustomNetworkConfig } from 'config';
export type TToggleOfflineConfig = typeof toggleOfflineConfig;
export function toggleOfflineConfig(): interfaces.ToggleOfflineAction {

View File

@ -1,5 +1,5 @@
import { TypeKeys } from './constants';
import { NodeConfig, CustomNodeConfig, NetworkConfig, CustomNetworkConfig } from 'config/data';
import { NodeConfig, CustomNodeConfig, NetworkConfig, CustomNetworkConfig } from 'config';
/*** Toggle Offline ***/
export interface ToggleOfflineAction {

View File

@ -1,4 +1,4 @@
import { Token } from 'config/data';
import { Token } from 'config';
import * as interfaces from './actionTypes';
import { TypeKeys } from './constants';

View File

@ -1,4 +1,4 @@
import { Token } from 'config/data';
import { Token } from 'config';
import { TypeKeys } from './constants';
/*** Add custom token ***/
export interface AddCustomTokenAction {

View File

@ -1,4 +1,4 @@
import bityConfig, { WhitelistedCoins } from 'config/bity';
import { WhitelistedCoins, bityConfig } from 'config';
import { checkHttpStatus, parseJSON, filter } from './utils';
import bitcoinIcon from 'assets/images/bitcoin.png';
import repIcon from 'assets/images/augur.png';

View File

@ -1,6 +1,6 @@
import React from 'react';
import { AddressFieldFactory } from './AddressFieldFactory';
import { donationAddressMap } from 'config/data';
import { donationAddressMap } from 'config';
interface Props {
isReadOnly?: boolean;

View File

@ -1,5 +1,5 @@
import { Identicon, UnitDisplay } from 'components/ui';
import { NetworkConfig } from 'config/data';
import { NetworkConfig } from 'config';
import { IWallet, Balance, TrezorWallet, LedgerWallet } from 'libs/wallet';
import React from 'react';
import translate from 'translations';
@ -127,11 +127,11 @@ export default class AccountInfo extends React.Component<Props, State> {
{!!blockExplorer && (
<li className="AccountInfo-list-item">
<a
href={blockExplorer.address(address)}
href={blockExplorer.addressUrl(address)}
target="_blank"
rel="noopener noreferrer"
>
{`${network.name} (${blockExplorer.name})`}
{`${network.name} (${blockExplorer.origin})`}
</a>
</li>
)}

View File

@ -5,7 +5,7 @@ import { State } from 'reducers/rates';
import { rateSymbols, TFetchCCRates } from 'actions/rates';
import { TokenBalance } from 'selectors/wallet';
import { Balance } from 'libs/wallet';
import { NetworkConfig } from 'config/data';
import { NetworkConfig } from 'config';
import { ETH_DECIMAL, convertTokenBase } from 'libs/units';
import Spinner from 'components/ui/Spinner';
import UnitDisplay from 'components/ui/UnitDisplay';

View File

@ -1,6 +1,6 @@
import React from 'react';
import classnames from 'classnames';
import { Token } from 'config/data';
import { Token } from 'config';
import { isPositiveIntegerOrZero, isValidETHAddress } from 'libs/validators';
import translate from 'translations';
import NewTabLink from 'components/ui/NewTabLink';

View File

@ -1,6 +1,6 @@
import React from 'react';
import translate from 'translations';
import { Token } from 'config/data';
import { Token } from 'config';
import { TokenBalance } from 'selectors/wallet';
import AddCustomTokenForm from './AddCustomTokenForm';
import TokenRow from './TokenRow';

View File

@ -15,7 +15,7 @@ import {
} from 'actions/wallet';
import { getAllTokens } from 'selectors/config';
import { getTokenBalances, getWalletInst, getWalletConfig, TokenBalance } from 'selectors/wallet';
import { Token } from 'config/data';
import { Token } from 'config';
import translate from 'translations';
import Balances from './Balances';
import Spinner from 'components/ui/Spinner';

View File

@ -1,5 +1,5 @@
import { fetchCCRates, TFetchCCRates } from 'actions/rates';
import { NetworkConfig } from 'config/data';
import { NetworkConfig } from 'config';
import { IWallet, Balance } from 'libs/wallet';
import React from 'react';
import { connect } from 'react-redux';

View File

@ -1,4 +1,4 @@
import { NodeConfig } from 'config/data';
import { NodeConfig } from 'config';
import React, { Component } from 'react';
import { AppState } from 'reducers';
import { connect } from 'react-redux';

View File

@ -1,7 +1,7 @@
import { DataFieldFactory } from './DataFieldFactory';
import React from 'react';
import translate from 'translations';
import { donationAddressMap } from 'config/data';
import { donationAddressMap } from 'config';
export const DataField: React.SFC<{}> = () => (
<DataFieldFactory

View File

@ -1,4 +1,4 @@
import { BlockExplorerConfig } from 'config/data';
import { BlockExplorerConfig } from 'config';
import React from 'react';
import { translateRaw } from 'translations';
@ -9,7 +9,7 @@ export interface TransactionSucceededProps {
const TransactionSucceeded = ({ txHash, blockExplorer }: TransactionSucceededProps) => {
// const checkTxLink = `https://www.myetherwallet.com?txHash=${txHash}/#check-tx-status`;
const txHashLink = blockExplorer.tx(txHash);
const txHashLink = blockExplorer.txUrl(txHash);
return (
<div>

View File

@ -7,7 +7,7 @@ import {
donationAddressMap,
VERSION,
knowledgeBaseURL
} from 'config/data';
} from 'config';
import React from 'react';
import translate from 'translations';
import './index.scss';

View File

@ -1,7 +1,7 @@
import React from 'react';
import Slider from 'rc-slider';
import translate from 'translations';
import { gasPriceDefaults } from 'config/data';
import { gasPriceDefaults } from 'config';
import FeeSummary from './FeeSummary';
import { TInputGasPrice } from 'actions/transaction';
import './SimpleGas.scss';

View File

@ -3,7 +3,7 @@ import { generateKeystoreFileInfo, KeystoreFile } from 'utils/keystore';
import Modal from 'components/ui/Modal';
import Input from './Input';
import translate, { translateRaw } from 'translations';
import { MINIMUM_PASSWORD_LENGTH } from 'config/data';
import { MINIMUM_PASSWORD_LENGTH } from 'config';
import { isValidPrivKey } from 'libs/validators';
import './index.scss';

View File

@ -2,7 +2,7 @@ import React from 'react';
import classnames from 'classnames';
import Modal, { IButton } from 'components/ui/Modal';
import translate from 'translations';
import { NETWORKS, CustomNodeConfig, CustomNetworkConfig } from 'config/data';
import { NETWORKS, CustomNodeConfig, CustomNetworkConfig } from 'config';
import { makeCustomNodeId } from 'utils/node';
import { makeCustomNetworkId } from 'utils/network';
@ -303,10 +303,16 @@ export default class CustomNodeModal extends React.Component<Props, State> {
}
private makeCustomNetworkConfigFromState(): CustomNetworkConfig {
const similarNetworkConfig = Object.values(NETWORKS).find(
n => n.chainId === +this.state.customNetworkChainId
);
const dPathFormats = similarNetworkConfig ? similarNetworkConfig.dPathFormats : null;
return {
name: this.state.customNetworkName,
unit: this.state.customNetworkUnit,
chainId: this.state.customNetworkChainId ? parseInt(this.state.customNetworkChainId, 10) : 0
chainId: this.state.customNetworkChainId ? parseInt(this.state.customNetworkChainId, 10) : 0,
dPathFormats
};
}
@ -352,6 +358,7 @@ export default class CustomNodeModal extends React.Component<Props, State> {
if (this.state.network === CUSTOM) {
const network = this.makeCustomNetworkConfigFromState();
this.props.handleAddCustomNetwork(network);
}

View File

@ -1,4 +1,4 @@
import { gasPriceDefaults, knowledgeBaseURL } from 'config/data';
import { gasPriceDefaults, knowledgeBaseURL } from 'config';
import throttle from 'lodash/throttle';
import React, { Component } from 'react';
import DropdownShell from 'components/ui/DropdownShell';

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import NavigationLink from './NavigationLink';
import { knowledgeBaseURL } from 'config/data';
import { knowledgeBaseURL } from 'config';
import './Navigation.scss';
export interface TabLink {

View File

@ -20,7 +20,7 @@ import {
NodeConfig,
CustomNodeConfig,
CustomNetworkConfig
} from 'config/data';
} from 'config';
import GasPriceDropdown from './components/GasPriceDropdown';
import Navigation from './components/Navigation';
import CustomNodeModal from './components/CustomNodeModal';

View File

@ -19,7 +19,6 @@ import {
import { reset, TReset } from 'actions/transaction';
import translate from 'translations';
import {
DigitalBitboxDecrypt,
KeystoreDecrypt,
LedgerNanoSDecrypt,
MnemonicDecrypt,
@ -31,38 +30,53 @@ import {
WalletButton
} from './components';
import { AppState } from 'reducers';
import { knowledgeBaseURL, isWeb3NodeAvailable } from 'config/data';
import { IWallet } from 'libs/wallet';
import DISABLES from './disables.json';
import DISABLES from './disables';
import { showNotification, TShowNotification } from 'actions/notifications';
import DigitalBitboxIcon from 'assets/images/wallets/digital-bitbox.svg';
import LedgerIcon from 'assets/images/wallets/ledger.svg';
import MetamaskIcon from 'assets/images/wallets/metamask.svg';
import MistIcon from 'assets/images/wallets/mist.svg';
import TrezorIcon from 'assets/images/wallets/trezor.svg';
import './WalletDecrypt.scss';
import {
SecureWalletName,
InsecureWalletName,
MiscWalletName,
WalletName,
isWeb3NodeAvailable,
knowledgeBaseURL
} from 'config';
import { unSupportedWalletFormatsOnNetwork } from 'utils/network';
import { getNetworkConfig } from '../../selectors/config';
interface Props {
resetTransactionState: TReset;
interface OwnProps {
hidden?: boolean;
disabledWallets?: WalletName[];
}
interface DispatchProps {
unlockKeystore: TUnlockKeystore;
unlockMnemonic: TUnlockMnemonic;
unlockPrivateKey: TUnlockPrivateKey;
setWallet: TSetWallet;
unlockWeb3: TUnlockWeb3;
setWallet: TSetWallet;
resetWallet: TResetWallet;
resetTransactionState: TReset;
showNotification: TShowNotification;
wallet: IWallet;
hidden?: boolean;
}
interface StateProps {
computedDisabledWallets: WalletName[];
offline: boolean;
disabledWallets?: string[];
isWalletPending: AppState['wallet']['isWalletPending'];
isPasswordPending: AppState['wallet']['isPasswordPending'];
}
type Props = OwnProps & StateProps & DispatchProps;
type UnlockParams = {} | PrivateKeyValue;
interface State {
selectedWalletKey: string | null;
selectedWalletKey: WalletName | null;
value: UnlockParams | null;
}
@ -71,13 +85,13 @@ interface BaseWalletInfo {
component: any;
initialParams: object;
unlock: any;
helpLink?: string;
helpLink: string;
isReadOnly?: boolean;
attemptUnlock?: boolean;
}
export interface SecureWalletInfo extends BaseWalletInfo {
icon?: string | null;
icon?: string;
description: string;
}
@ -85,6 +99,9 @@ export interface InsecureWalletInfo extends BaseWalletInfo {
example: string;
}
// tslint:disable-next-line:no-empty-interface
interface MiscWalletInfo extends InsecureWalletInfo {}
const WEB3_TYPES = {
MetamaskInpageProvider: {
lid: 'x_MetaMask',
@ -95,15 +112,20 @@ const WEB3_TYPES = {
icon: MistIcon
}
};
type SecureWallets = { [key in SecureWalletName]: SecureWalletInfo };
type InsecureWallets = { [key in InsecureWalletName]: InsecureWalletInfo };
type MiscWallet = { [key in MiscWalletName]: MiscWalletInfo };
type Wallets = SecureWallets & InsecureWallets & MiscWallet;
const WEB3_TYPE: string | false =
(window as any).web3 && (window as any).web3.currentProvider.constructor.name;
const SECURE_WALLETS = ['web3', 'ledger-nano-s', 'trezor'];
const INSECURE_WALLETS = ['private-key', 'keystore-file', 'mnemonic-phrase'];
export class WalletDecrypt extends Component<Props, State> {
public WALLETS: { [key: string]: SecureWalletInfo | InsecureWalletInfo } = {
web3: {
// https://github.com/Microsoft/TypeScript/issues/13042
// index signature should become [key: Wallets] (from config) once typescript bug is fixed
public WALLETS: Wallets = {
[SecureWalletName.WEB3]: {
lid: WEB3_TYPE ? WEB3_TYPES[WEB3_TYPE].lid : 'x_Web3',
icon: WEB3_TYPE && WEB3_TYPES[WEB3_TYPE].icon,
description: 'ADD_Web3Desc',
@ -113,7 +135,7 @@ export class WalletDecrypt extends Component<Props, State> {
attemptUnlock: true,
helpLink: `${knowledgeBaseURL}/migration/moving-from-private-key-to-metamask`
},
'ledger-nano-s': {
[SecureWalletName.LEDGER_NANO_S]: {
lid: 'x_Ledger',
icon: LedgerIcon,
description: 'ADD_HardwareDesc',
@ -123,7 +145,7 @@ export class WalletDecrypt extends Component<Props, State> {
helpLink:
'https://ledger.zendesk.com/hc/en-us/articles/115005200009-How-to-use-MyEtherWallet-with-Ledger'
},
trezor: {
[SecureWalletName.TREZOR]: {
lid: 'x_Trezor',
icon: TrezorIcon,
description: 'ADD_HardwareDesc',
@ -132,16 +154,7 @@ export class WalletDecrypt extends Component<Props, State> {
unlock: this.props.setWallet,
helpLink: 'https://doc.satoshilabs.com/trezor-apps/mew.html'
},
'digital-bitbox': {
lid: 'x_DigitalBitbox',
icon: DigitalBitboxIcon,
description: 'ADD_HardwareDesc',
component: DigitalBitboxDecrypt,
initialParams: {},
unlock: this.props.setWallet,
helpLink: 'https://digitalbitbox.com/ethereum'
},
'keystore-file': {
[InsecureWalletName.KEYSTORE_FILE]: {
lid: 'x_Keystore2',
example: 'UTC--2017-12-15T17-35-22.547Z--6be6e49e82425a5aa56396db03512f2cc10e95e8',
component: KeystoreDecrypt,
@ -152,7 +165,7 @@ export class WalletDecrypt extends Component<Props, State> {
unlock: this.props.unlockKeystore,
helpLink: `${knowledgeBaseURL}/private-keys-passwords/difference-beween-private-key-and-keystore-file.html`
},
'mnemonic-phrase': {
[InsecureWalletName.MNEMONIC_PHRASE]: {
lid: 'x_Mnemonic',
example: 'brain surround have swap horror cheese file distinct',
component: MnemonicDecrypt,
@ -160,7 +173,7 @@ export class WalletDecrypt extends Component<Props, State> {
unlock: this.props.unlockMnemonic,
helpLink: `${knowledgeBaseURL}/private-keys-passwords/difference-beween-private-key-and-keystore-file.html`
},
'private-key': {
[InsecureWalletName.PRIVATE_KEY]: {
lid: 'x_PrivKey2',
example: 'f1d0e0789c6d40f399ca90cc674b7858de4c719e0d5752a60d5d2f6baa45d4c9',
component: PrivateKeyDecrypt,
@ -171,7 +184,7 @@ export class WalletDecrypt extends Component<Props, State> {
unlock: this.props.unlockPrivateKey,
helpLink: `${knowledgeBaseURL}/private-keys-passwords/difference-beween-private-key-and-keystore-file.html`
},
'view-only': {
[MiscWalletName.VIEW_ONLY]: {
lid: 'View Address',
example: '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8',
component: ViewOnlyDecrypt,
@ -181,12 +194,13 @@ export class WalletDecrypt extends Component<Props, State> {
isReadOnly: true
}
};
public state: State = {
selectedWalletKey: null,
value: null
};
public componentWillReceiveProps(nextProps) {
public componentWillReceiveProps(nextProps: Props) {
// Reset state when unlock is hidden / revealed
if (nextProps.hidden !== this.props.hidden) {
this.setState({
@ -218,10 +232,12 @@ export class WalletDecrypt extends Component<Props, State> {
onUnlock={this.onUnlock}
showNotification={this.props.showNotification}
isWalletPending={
this.state.selectedWalletKey === 'keystore-file' ? this.props.isWalletPending : undefined
this.state.selectedWalletKey === InsecureWalletName.KEYSTORE_FILE
? this.props.isWalletPending
: undefined
}
isPasswordPending={
this.state.selectedWalletKey === 'keystore-file'
this.state.selectedWalletKey === InsecureWalletName.KEYSTORE_FILE
? this.props.isPasswordPending
: undefined
}
@ -230,64 +246,72 @@ export class WalletDecrypt extends Component<Props, State> {
}
public buildWalletOptions() {
const viewOnly = this.WALLETS['view-only'] as InsecureWalletInfo;
const SECURE_WALLETS = Object.values(SecureWalletName);
const INSECURE_WALLETS = Object.values(InsecureWalletName);
const MISC_WALLETS = Object.values(MiscWalletName);
return (
<div className="WalletDecrypt-wallets">
<h2 className="WalletDecrypt-wallets-title">{translate('decrypt_Access')}</h2>
<div className="WalletDecrypt-wallets-row">
{SECURE_WALLETS.map(type => {
const wallet = this.WALLETS[type] as SecureWalletInfo;
{SECURE_WALLETS.map((walletType: SecureWalletName) => {
const wallet = this.WALLETS[walletType];
return (
<WalletButton
key={type}
key={walletType}
name={translate(wallet.lid)}
description={translate(wallet.description)}
icon={wallet.icon}
helpLink={wallet.helpLink}
walletType={type}
walletType={walletType}
isSecure={true}
isDisabled={this.isWalletDisabled(type)}
isDisabled={this.isWalletDisabled(walletType)}
onClick={this.handleWalletChoice}
/>
);
})}
</div>
<div className="WalletDecrypt-wallets-row">
{INSECURE_WALLETS.map(type => {
const wallet = this.WALLETS[type] as InsecureWalletInfo;
{INSECURE_WALLETS.map((walletType: InsecureWalletName) => {
const wallet = this.WALLETS[walletType];
return (
<WalletButton
key={type}
key={walletType}
name={translate(wallet.lid)}
example={wallet.example}
helpLink={wallet.helpLink}
walletType={type}
walletType={walletType}
isSecure={false}
isDisabled={this.isWalletDisabled(type)}
isDisabled={this.isWalletDisabled(walletType)}
onClick={this.handleWalletChoice}
/>
);
})}
<WalletButton
key="view-only"
name={translate(viewOnly.lid)}
example={viewOnly.example}
helpLink={viewOnly.helpLink}
walletType="view-only"
isReadOnly={true}
isDisabled={this.isWalletDisabled('view-only')}
onClick={this.handleWalletChoice}
/>
{MISC_WALLETS.map((walletType: MiscWalletName) => {
const wallet = this.WALLETS[walletType];
return (
<WalletButton
key={walletType}
name={translate(wallet.lid)}
example={wallet.example}
helpLink={wallet.helpLink}
walletType={walletType}
isReadOnly={true}
isDisabled={this.isWalletDisabled(walletType)}
onClick={this.handleWalletChoice}
/>
);
})}
</div>
</div>
);
}
public handleWalletChoice = async (walletType: string) => {
public handleWalletChoice = async (walletType: WalletName) => {
const wallet = this.WALLETS[walletType];
if (!wallet) {
return;
}
@ -301,7 +325,7 @@ export class WalletDecrypt extends Component<Props, State> {
wallet.unlock();
}
setTimeout(() => {
window.setTimeout(() => {
this.setState({
selectedWalletKey: walletType,
value: wallet.initialParams
@ -375,28 +399,31 @@ export class WalletDecrypt extends Component<Props, State> {
this.props.resetTransactionState();
};
private isWalletDisabled = (walletKey: string) => {
private isWalletDisabled = (walletKey: WalletName) => {
if (this.props.offline && DISABLES.ONLINE_ONLY.includes(walletKey)) {
return true;
}
if (!this.props.disabledWallets) {
return false;
}
return this.props.disabledWallets.indexOf(walletKey) !== -1;
return this.props.computedDisabledWallets.indexOf(walletKey) !== -1;
};
}
function mapStateToProps(state: AppState) {
function mapStateToProps(state: AppState, ownProps: Props) {
const { disabledWallets } = ownProps;
const network = getNetworkConfig(state);
const networkDisabledFormats = unSupportedWalletFormatsOnNetwork(network);
const computedDisabledWallets = disabledWallets
? disabledWallets.concat(networkDisabledFormats)
: networkDisabledFormats;
return {
computedDisabledWallets,
offline: state.config.offline,
wallet: state.wallet.inst,
isWalletPending: state.wallet.isWalletPending,
isPasswordPending: state.wallet.isPasswordPending
};
}
export default connect(mapStateToProps, {
export default connect<StateProps, DispatchProps>(mapStateToProps, {
unlockKeystore,
unlockMnemonic,
unlockPrivateKey,

View File

@ -1,15 +1,14 @@
@import "common/sass/variables";
@import "common/sass/mixins";
@import 'common/sass/variables';
@import 'common/sass/mixins';
.DWModal {
width: 690px;
&-path {
display: block;
display: flex;
margin-bottom: 20px;
&-label {
font-size: $font-size-medium;
margin-right: 16px;
}
.form-control {
@ -17,12 +16,18 @@
width: auto;
margin: 0 0 0 10px;
}
.Select {
flex-grow: 1;
}
}
&-addresses {
overflow-y: scroll;
&-table {
width: 100%;
width: 695px;
text-align: center;
margin: auto;
margin-bottom: 10px;
&-token {

View File

@ -8,7 +8,7 @@ import {
} from 'actions/deterministicWallets';
import Modal, { IButton } from 'components/ui/Modal';
import { AppState } from 'reducers';
import { NetworkConfig } from 'config/data';
import { NetworkConfig } from 'config';
import { isValidPath } from 'libs/validators';
import React from 'react';
import { connect } from 'react-redux';
@ -16,6 +16,8 @@ import { getNetworkConfig } from 'selectors/config';
import { getTokens, MergedToken } from 'selectors/wallet';
import { UnitDisplay } from 'components/ui';
import './DeterministicWalletsModal.scss';
import { DPath } from 'config/dpaths';
import Select from 'react-select';
const WALLETS_PER_PAGE = 5;
@ -24,7 +26,7 @@ interface Props {
isOpen?: boolean;
walletType?: string;
dPath: string;
dPaths: { label: string; value: string }[];
dPaths: DPath[];
publicKey?: string;
chainCode?: string;
seed?: string;
@ -45,6 +47,7 @@ interface Props {
}
interface State {
currentLabel: string;
selectedAddress: string;
selectedAddrIndex: number;
isCustomPath: boolean;
@ -52,12 +55,18 @@ interface State {
page: number;
}
const customDPath: DPath = {
label: 'custom',
value: 'custom'
};
class DeterministicWalletsModalClass extends React.Component<Props, State> {
public state = {
public state: State = {
selectedAddress: '',
selectedAddrIndex: 0,
isCustomPath: false,
customPath: '',
currentLabel: '',
page: 0
};
@ -88,7 +97,7 @@ class DeterministicWalletsModalClass extends React.Component<Props, State> {
onCancel,
walletType
} = this.props;
const { selectedAddress, isCustomPath, customPath, page } = this.state;
const { selectedAddress, customPath, page } = this.state;
const validPathClass = isValidPath(customPath) ? 'is-valid' : 'is-invalid';
const buttons: IButton[] = [
@ -113,21 +122,20 @@ class DeterministicWalletsModalClass extends React.Component<Props, State> {
handleClose={onCancel}
>
<div className="DWModal">
{/* TODO: replace styles for flexbox with flexbox classes in https://github.com/MyEtherWallet/MyEtherWallet/pull/850/files#diff-2150778b9391533fec7b8afd060c7672 */}
<form className="DWModal-path form-group-sm" onSubmit={this.handleSubmitCustomPath}>
<span className="DWModal-path-label">Addresses for</span>
<select
className="form-control"
<span className="DWModal-path-label">Addresses </span>
<Select
name="fieldDPath"
className=""
value={this.state.currentLabel || this.findDPath('value', dPath).value}
onChange={this.handleChangePath}
value={isCustomPath ? 'custom' : dPath}
>
{dPaths.map(dp => (
<option key={dp.value} value={dp.value}>
{dp.label}
</option>
))}
<option value="custom">Custom path...</option>
</select>
{isCustomPath && (
options={dPaths}
clearable={false}
searchable={false}
/>
{/* TODO/Hack - Custom Paths are temporarily disabled. `false` is used for smallest diff */}
{false && (
<input
className={`form-control ${validPathClass}`}
value={customPath}
@ -163,22 +171,21 @@ class DeterministicWalletsModalClass extends React.Component<Props, State> {
</thead>
<tbody>{wallets.map(wallet => this.renderWalletRow(wallet))}</tbody>
</table>
<div className="DWModal-addresses-nav">
<button
className="DWModal-addresses-nav-btn btn btn-sm btn-default"
disabled={page === 0}
onClick={this.prevPage}
>
Back
</button>
<button
className="DWModal-addresses-nav-btn btn btn-sm btn-default"
onClick={this.nextPage}
>
More
</button>
</div>
</div>
<div className="DWModal-addresses-nav">
<button
className="DWModal-addresses-nav-btn btn btn-sm btn-default"
disabled={page === 0}
onClick={this.prevPage}
>
Back
</button>
<button
className="DWModal-addresses-nav-btn btn btn-sm btn-default"
onClick={this.nextPage}
>
More
</button>
</div>
</div>
</Modal>
@ -200,16 +207,19 @@ class DeterministicWalletsModalClass extends React.Component<Props, State> {
}
}
private handleChangePath = (ev: React.FormEvent<HTMLSelectElement>) => {
const { value } = ev.currentTarget;
private findDPath = (prop: keyof DPath, cmp: string) => {
return this.props.dPaths.find(d => d[prop] === cmp) || customDPath;
};
private handleChangePath = (newPath: DPath) => {
const { value: dPathLabel } = newPath;
const { value } = this.findDPath('value', dPathLabel);
if (value === 'custom') {
this.setState({ isCustomPath: true });
this.setState({ isCustomPath: true, currentLabel: dPathLabel });
} else {
this.setState({ isCustomPath: false });
if (this.props.dPath !== value) {
this.props.onPathChange(value);
}
this.setState({ isCustomPath: false, currentLabel: dPathLabel });
this.props.onPathChange(value);
}
};

View File

@ -4,15 +4,22 @@ import translate, { translateRaw } from 'translations';
import DeterministicWalletsModal from './DeterministicWalletsModal';
import { LedgerWallet } from 'libs/wallet';
import ledger from 'ledgerco';
import DPATHS from 'config/dpaths';
import { Spinner } from 'components/ui';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getNetworkConfig } from 'selectors/config';
import { SecureWalletName } from 'config';
import { DPath } from 'config/dpaths';
import { getPaths, getSingleDPath } from 'utils/network';
const DEFAULT_PATH = DPATHS.LEDGER[0].value;
interface Props {
interface OwnProps {
onUnlock(param: any): void;
}
interface StateProps {
dPath: DPath;
}
interface State {
publicKey: string;
chainCode: string;
@ -22,11 +29,13 @@ interface State {
showTip: boolean;
}
export class LedgerNanoSDecrypt extends Component<Props, State> {
type Props = OwnProps & StateProps;
class LedgerNanoSDecryptClass extends Component<Props, State> {
public state: State = {
publicKey: '',
chainCode: '',
dPath: DEFAULT_PATH,
dPath: this.props.dPath.value,
error: null,
isLoading: false,
showTip: false
@ -114,7 +123,7 @@ export class LedgerNanoSDecrypt extends Component<Props, State> {
publicKey={publicKey}
chainCode={chainCode}
dPath={dPath}
dPaths={DPATHS.LEDGER}
dPaths={getPaths(SecureWalletName.LEDGER_NANO_S)}
onCancel={this.handleCancel}
onConfirmAddress={this.handleUnlock}
onPathChange={this.handlePathChange}
@ -146,11 +155,11 @@ export class LedgerNanoSDecrypt extends Component<Props, State> {
});
})
.catch(err => {
if (err.metaData.code === 5) {
if (err.metaData && err.metaData.code === 5) {
this.showTip();
}
this.setState({
error: err.metaData.type,
error: err.metaData ? err.metaData.type : err,
isLoading: false
});
});
@ -174,7 +183,16 @@ export class LedgerNanoSDecrypt extends Component<Props, State> {
this.setState({
publicKey: '',
chainCode: '',
dPath: DEFAULT_PATH
dPath: this.props.dPath.value
});
}
}
function mapStateToProps(state: AppState): StateProps {
const network = getNetworkConfig(state);
return {
dPath: getSingleDPath(SecureWalletName.LEDGER_NANO_S, network)
};
}
export const LedgerNanoSDecrypt = connect(mapStateToProps)(LedgerNanoSDecryptClass);

View File

@ -1,15 +1,23 @@
import { mnemonicToSeed, validateMnemonic } from 'bip39';
import DPATHS from 'config/dpaths';
import React, { Component } from 'react';
import translate, { translateRaw } from 'translations';
import DeterministicWalletsModal from './DeterministicWalletsModal';
import { formatMnemonic } from 'utils/formatters';
const DEFAULT_PATH = DPATHS.MNEMONIC[0].value;
import { InsecureWalletName } from 'config';
import { AppState } from 'reducers';
import { getNetworkConfig } from 'selectors/config';
import { connect } from 'react-redux';
import { DPath } from 'config/dpaths';
import { getPaths, getSingleDPath } from 'utils/network';
interface Props {
onUnlock(param: any): void;
}
interface StateProps {
dPath: DPath;
}
interface State {
phrase: string;
formattedPhrase: string;
@ -18,13 +26,13 @@ interface State {
dPath: string;
}
export class MnemonicDecrypt extends Component<Props, State> {
class MnemonicDecryptClass extends Component<Props & StateProps, State> {
public state: State = {
phrase: '',
formattedPhrase: '',
pass: '',
seed: '',
dPath: DEFAULT_PATH
dPath: this.props.dPath.value
};
public render() {
@ -70,7 +78,7 @@ export class MnemonicDecrypt extends Component<Props, State> {
isOpen={!!seed}
seed={seed}
dPath={dPath}
dPaths={DPATHS.MNEMONIC}
dPaths={getPaths(InsecureWalletName.MNEMONIC_PHRASE)}
onCancel={this.handleCancel}
onConfirmAddress={this.handleUnlock}
onPathChange={this.handlePathChange}
@ -135,3 +143,12 @@ export class MnemonicDecrypt extends Component<Props, State> {
});
};
}
function mapStateToProps(state: AppState): StateProps {
const network = getNetworkConfig(state);
return {
dPath: getSingleDPath(InsecureWalletName.MNEMONIC_PHRASE, network)
};
}
export const MnemonicDecrypt = connect(mapStateToProps)(MnemonicDecryptClass);

View File

@ -1,4 +1,3 @@
import DPATHS from 'config/dpaths';
import { TrezorWallet, TREZOR_MINIMUM_FIRMWARE } from 'libs/wallet';
import React, { Component } from 'react';
import translate, { translateRaw } from 'translations';
@ -6,11 +5,23 @@ import TrezorConnect from 'vendor/trezor-connect';
import DeterministicWalletsModal from './DeterministicWalletsModal';
import './Trezor.scss';
import { Spinner } from 'components/ui';
const DEFAULT_PATH = DPATHS.TREZOR[0].value;
import { getNetworkConfig } from 'selectors/config';
import { AppState } from 'reducers';
import { connect } from 'react-redux';
import { SecureWalletName } from 'config';
import { DPath } from 'config/dpaths';
import { getPaths, getSingleDPath } from 'utils/network';
interface Props {
//todo: conflicts with comment in walletDecrypt -> onUnlock method
interface OwnProps {
onUnlock(param: any): void;
}
interface StateProps {
dPath: DPath;
}
// todo: nearly duplicates ledger component props
interface State {
publicKey: string;
chainCode: string;
@ -19,11 +30,13 @@ interface State {
isLoading: boolean;
}
export class TrezorDecrypt extends Component<Props, State> {
type Props = OwnProps & StateProps;
class TrezorDecryptClass extends Component<Props, State> {
public state: State = {
publicKey: '',
chainCode: '',
dPath: DEFAULT_PATH,
dPath: this.props.dPath.value,
error: null,
isLoading: false
};
@ -76,7 +89,7 @@ export class TrezorDecrypt extends Component<Props, State> {
publicKey={publicKey}
chainCode={chainCode}
dPath={dPath}
dPaths={DPATHS.TREZOR}
dPaths={getPaths(SecureWalletName.TREZOR)}
onCancel={this.handleCancel}
onConfirmAddress={this.handleUnlock}
onPathChange={this.handlePathChange}
@ -133,7 +146,16 @@ export class TrezorDecrypt extends Component<Props, State> {
this.setState({
publicKey: '',
chainCode: '',
dPath: DEFAULT_PATH
dPath: this.props.dPath.value
});
}
}
function mapStateToProps(state: AppState): StateProps {
const network = getNetworkConfig(state);
return {
dPath: getSingleDPath(SecureWalletName.TREZOR, network)
};
}
export const TrezorDecrypt = connect(mapStateToProps)(TrezorDecryptClass);

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import translate from 'translations';
import { donationAddressMap } from 'config/data';
import { donationAddressMap } from 'config';
import { isValidETHAddress } from 'libs/validators';
import { AddressOnlyWallet } from 'libs/wallet';

View File

@ -1,23 +1,31 @@
import React from 'react';
import classnames from 'classnames';
import { translateRaw } from 'translations';
import { translateRaw, TranslateType } from 'translations';
import { NewTabLink, Tooltip } from 'components/ui';
import './WalletButton.scss';
interface Props {
name: React.ReactElement<string> | string;
description?: React.ReactElement<string> | string;
example?: React.ReactElement<string> | string;
icon?: string | null;
helpLink?: string;
walletType: string;
import { WalletName } from 'config';
interface OwnProps {
name: TranslateType;
description?: TranslateType;
example?: TranslateType;
icon?: string;
helpLink: string;
walletType: WalletName;
isSecure?: boolean;
isReadOnly?: boolean;
isDisabled?: boolean;
onClick(walletType: string): void;
}
export class WalletButton extends React.Component<Props, {}> {
interface StateProps {
isFormatDisabled?: boolean;
}
type Props = OwnProps & StateProps;
export class WalletButton extends React.PureComponent<Props> {
public render() {
const {
name,
@ -45,27 +53,29 @@ export class WalletButton extends React.Component<Props, {}> {
{icon && <img className="WalletButton-title-icon" src={icon} />}
<span>{name}</span>
</div>
{description && <div className="WalletButton-description">{description}</div>}
{example && <div className="WalletButton-example">{example}</div>}
<div className="WalletButton-icons">
{isSecure === true && (
{isSecure ? (
<span className="WalletButton-icons-icon" onClick={this.stopPropogation}>
<i className="fa fa-shield" />
<Tooltip>{translateRaw('This wallet type is secure')}</Tooltip>
</span>
)}
{isSecure === false && (
) : (
<span className="WalletButton-icons-icon" onClick={this.stopPropogation}>
<i className="fa fa-exclamation-triangle" />
<Tooltip>{translateRaw('This wallet type is insecure')}</Tooltip>
</span>
)}
{isReadOnly === true && (
{isReadOnly && (
<span className="WalletButton-icons-icon" onClick={this.stopPropogation}>
<i className="fa fa-eye" />
<Tooltip>{translateRaw('You cannot send using address only')}</Tooltip>
</span>
)}
{helpLink && (
<span className="WalletButton-icons-icon">
<NewTabLink href={helpLink} onClick={this.stopPropogation}>
@ -80,14 +90,14 @@ export class WalletButton extends React.Component<Props, {}> {
}
private handleClick = () => {
if (this.props.isDisabled) {
if (this.props.isDisabled || this.props.isFormatDisabled) {
return;
}
this.props.onClick(this.props.walletType);
};
private stopPropogation = (ev: React.SyntheticEvent<any>) => {
private stopPropogation = (ev: React.FormEvent<HTMLAnchorElement | HTMLSpanElement>) => {
ev.stopPropagation();
};
}

View File

@ -1,5 +0,0 @@
{
"READ_ONLY": ["view-only"],
"UNABLE_TO_SIGN": ["trezor", "view-only"],
"ONLINE_ONLY": ["web3", "trezor"]
}

View File

@ -0,0 +1,15 @@
import { MiscWalletName, SecureWalletName, WalletName } from 'config';
enum WalletMode {
READ_ONLY = 'READ_ONLY',
UNABLE_TO_SIGN = 'UNABLE_TO_SIGN',
ONLINE_ONLY = 'ONLINE_ONLY'
}
const walletModes: { [key in WalletMode]: WalletName[] } = {
[WalletMode.READ_ONLY]: [MiscWalletName.VIEW_ONLY],
[WalletMode.UNABLE_TO_SIGN]: [SecureWalletName.TREZOR, MiscWalletName.VIEW_ONLY],
[WalletMode.ONLINE_ONLY]: [SecureWalletName.WEB3, SecureWalletName.TREZOR]
};
export default walletModes;

View File

@ -1,3 +1,3 @@
import WalletDecrypt from './WalletDecrypt';
export default WalletDecrypt;
export { default as DISABLE_WALLETS } from './disables.json';
export { default as DISABLE_WALLETS } from './disables';

View File

@ -1,20 +1,23 @@
import React from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import translate from 'translations';
import translate, { TranslateType } from 'translations';
import WalletDecrypt from 'components/WalletDecrypt';
import { IWallet } from 'libs/wallet/IWallet';
import './UnlockHeader.scss';
import { WalletName } from 'config';
interface Props {
title: React.ReactElement<string> | string;
title: TranslateType;
wallet: IWallet;
disabledWallets?: string[];
disabledWallets?: WalletName[];
}
interface State {
isExpanded: boolean;
}
export class UnlockHeader extends React.Component<Props, State> {
export class UnlockHeader extends React.PureComponent<Props, State> {
public state = {
isExpanded: !this.props.wallet
};
@ -57,7 +60,7 @@ export class UnlockHeader extends React.Component<Props, State> {
);
}
public toggleisExpanded = () => {
public toggleisExpanded = (_: React.FormEvent<HTMLButtonElement>) => {
this.setState(state => {
return { isExpanded: !state.isExpanded };
});

View File

@ -26,7 +26,7 @@ export function generateKindMax(BTCKINDRate: number, kind: keyof typeof buffers)
return kindMax - kindMax * buffers[kind];
}
const info = {
export const bityConfig = {
serverURL,
bityURL,
ETHTxExplorer,
@ -41,5 +41,3 @@ const info = {
}
}
};
export default info;

View File

@ -1,6 +1,7 @@
import { EtherscanNode, InfuraNode, RPCNode, Web3Node } from 'libs/nodes';
import { networkIdToName } from 'libs/values';
import { getValues } from '../utils/helpers';
export const languages = require('./languages.json');
// Displays in the header
export const VERSION = '4.0.0 (Alpha 0.1.0)';
export const N_FACTOR = 1024;
@ -24,7 +25,7 @@ export const ANNOUNCEMENT_MESSAGE = `
const etherScan = 'https://etherscan.io';
const blockChainInfo = 'https://blockchain.info';
const ethPlorer = 'https://ethplorer.io';
export const ethPlorer = 'https://ethplorer.io';
export const ETHTxExplorer = (txHash: string): string => `${etherScan}/tx/${txHash}`;
export const BTCTxExplorer = (txHash: string): string => `${blockChainInfo}/tx/${txHash}`;
@ -52,217 +53,32 @@ export const ledgerReferralURL = 'https://www.ledgerwallet.com/r/fa4b?path=/prod
export const trezorReferralURL = 'https://trezor.io/?a=myetherwallet.com';
export const bitboxReferralURL = 'https://digitalbitbox.com/?ref=mew';
export interface BlockExplorerConfig {
name: string;
tx(txHash: string): string;
address(address: string): string;
export enum SecureWalletName {
WEB3 = 'web3',
LEDGER_NANO_S = 'ledgerNanoS',
TREZOR = 'trezor'
}
export interface Token {
address: string;
symbol: string;
decimal: number;
error?: string | null;
export enum HardwareWalletName {
LEDGER_NANO_S = 'ledgerNanoS',
TREZOR = 'trezor'
}
export interface NetworkContract {
name: string;
address?: string;
abi: string;
export enum InsecureWalletName {
PRIVATE_KEY = 'privateKey',
KEYSTORE_FILE = 'keystoreFile',
MNEMONIC_PHRASE = 'mnemonicPhrase'
}
export interface NetworkConfig {
name: string;
unit: string;
color?: string;
blockExplorer?: BlockExplorerConfig;
tokenExplorer?: {
name: string;
address(address: string): string;
};
chainId: number;
tokens: Token[];
contracts: NetworkContract[] | null;
isTestnet?: boolean;
export enum MiscWalletName {
VIEW_ONLY = 'viewOnly'
}
export interface CustomNetworkConfig {
name: string;
unit: string;
chainId: number;
}
export const walletNames = getValues(
SecureWalletName,
HardwareWalletName,
InsecureWalletName,
MiscWalletName
);
export interface NodeConfig {
network: string;
lib: RPCNode | Web3Node;
service: string;
estimateGas?: boolean;
hidden?: boolean;
}
export interface CustomNodeConfig {
name: string;
url: string;
port: number;
network: string;
auth?: {
username: string;
password: string;
};
}
// Must be a website that follows the ethplorer convention of /tx/[hash] and
// address/[address] to generate the correct functions.
function makeExplorer(url): BlockExplorerConfig {
return {
name: url,
tx: hash => `${url}/tx/${hash}`,
address: address => `${url}/address/${address}`
};
}
export const NETWORKS: { [key: string]: NetworkConfig } = {
ETH: {
name: 'ETH',
unit: 'ETH',
chainId: 1,
color: '#0e97c0',
blockExplorer: makeExplorer('https://etherscan.io'),
tokenExplorer: {
name: ethPlorer,
address: ETHTokenExplorer
},
tokens: require('./tokens/eth.json'),
contracts: require('./contracts/eth.json')
},
Ropsten: {
name: 'Ropsten',
unit: 'ETH',
chainId: 3,
color: '#adc101',
blockExplorer: makeExplorer('https://ropsten.etherscan.io'),
tokens: require('./tokens/ropsten.json'),
contracts: require('./contracts/ropsten.json'),
isTestnet: true
},
Kovan: {
name: 'Kovan',
unit: 'ETH',
chainId: 42,
color: '#adc101',
blockExplorer: makeExplorer('https://kovan.etherscan.io'),
tokens: require('./tokens/ropsten.json'),
contracts: require('./contracts/ropsten.json'),
isTestnet: true
},
Rinkeby: {
name: 'Rinkeby',
unit: 'ETH',
chainId: 4,
color: '#adc101',
blockExplorer: makeExplorer('https://rinkeby.etherscan.io'),
tokens: require('./tokens/rinkeby.json'),
contracts: require('./contracts/rinkeby.json'),
isTestnet: true
}
};
export const NODES: { [key: string]: NodeConfig } = {
eth_mew: {
network: 'ETH',
lib: new RPCNode('https://api.myetherapi.com/eth'),
service: 'MyEtherWallet',
estimateGas: true
},
eth_ethscan: {
network: 'ETH',
service: 'Etherscan.io',
lib: new EtherscanNode('https://api.etherscan.io/api'),
estimateGas: false
},
eth_infura: {
network: 'ETH',
service: 'infura.io',
lib: new InfuraNode('https://mainnet.infura.io/mew'),
estimateGas: false
},
rop_mew: {
network: 'Ropsten',
service: 'MyEtherWallet',
lib: new RPCNode('https://api.myetherapi.com/rop'),
estimateGas: false
},
rop_infura: {
network: 'Ropsten',
service: 'infura.io',
lib: new InfuraNode('https://ropsten.infura.io/mew'),
estimateGas: false
},
kov_ethscan: {
network: 'Kovan',
service: 'Etherscan.io',
lib: new EtherscanNode('https://kovan.etherscan.io/api'),
estimateGas: false
},
rin_ethscan: {
network: 'Rinkeby',
service: 'Etherscan.io',
lib: new EtherscanNode('https://rinkeby.etherscan.io/api'),
estimateGas: false
},
rin_infura: {
network: 'Rinkeby',
service: 'infura.io',
lib: new InfuraNode('https://rinkeby.infura.io/mew'),
estimateGas: false
}
};
interface Web3NodeInfo {
networkId: string;
lib: Web3Node;
}
export async function setupWeb3Node(): Promise<Web3NodeInfo> {
const { web3 } = window as any;
if (!web3 || !web3.currentProvider || !web3.currentProvider.sendAsync) {
throw new Error(
'Web3 not found. Please check that MetaMask is installed, or that MyEtherWallet is open in Mist.'
);
}
const lib = new Web3Node();
const networkId = await lib.getNetVersion();
const accounts = await lib.getAccounts();
if (!accounts.length) {
throw new Error('No accounts found in MetaMask / Mist.');
}
if (networkId === 'loading') {
throw new Error('MetaMask / Mist is still loading. Please refresh the page and try again.');
}
return { networkId, lib };
}
export async function isWeb3NodeAvailable(): Promise<boolean> {
try {
await setupWeb3Node();
return true;
} catch (e) {
return false;
}
}
export async function initWeb3Node(): Promise<void> {
const { networkId, lib } = await setupWeb3Node();
NODES.web3 = {
network: networkIdToName(networkId),
service: 'MetaMask / Mist',
lib,
estimateGas: false,
hidden: true
};
}
export type WalletName = SecureWalletName | InsecureWalletName | MiscWalletName;

View File

@ -1,46 +1,52 @@
const ETH_DEFAULT = {
export interface DPath {
label: string;
value: string; // TODO determine method for more precise typing for path
}
export const ETH_DEFAULT: DPath = {
label: 'Default (ETH)',
value: "m/44'/60'/0'/0"
};
const ETH_TREZOR = {
export const ETH_TREZOR: DPath = {
label: 'TREZOR (ETH)',
value: "m/44'/60'/0'/0"
};
const ETH_LEDGER = {
export const ETH_LEDGER: DPath = {
label: 'Ledger (ETH)',
value: "m/44'/60'/0'"
};
const ETC_LEDGER = {
export const ETC_LEDGER: DPath = {
label: 'Ledger (ETC)',
value: "m/44'/60'/160720'/0'"
};
const ETC_TREZOR = {
export const ETC_TREZOR: DPath = {
label: 'TREZOR (ETC)',
value: "m/44'/61'/0'/0"
};
const TESTNET = {
label: 'Testnet',
export const ETH_TESTNET: DPath = {
label: 'Testnet (ETH)',
value: "m/44'/1'/0'/0"
};
const EXPANSE = {
label: 'Expanse',
export const EXP_DEFAULT: DPath = {
label: 'Default (EXP)',
value: "m/44'/40'/0'/0"
};
const TREZOR = [ETH_TREZOR, ETC_TREZOR, TESTNET];
const MNEMONIC = [ETH_DEFAULT, ETH_LEDGER, ETC_LEDGER, ETC_TREZOR, TESTNET, EXPANSE];
const LEDGER = [ETH_LEDGER, ETC_LEDGER, TESTNET];
export default {
TREZOR,
MNEMONIC,
LEDGER
export const UBQ_DEFAULT: DPath = {
label: 'Default (UBQ)',
value: "m/44'/108'/0'/0"
};
export const ETH_SINGULAR: DPath = {
label: 'SingularDTV',
value: "m/0'/0'/0'"
};
// PATHS TO BE INCLUDED REGARDLESS OF WALLET FORMAT
export const EXTRA_PATHS = [ETH_SINGULAR];

3
common/config/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './networks';
export * from './data';
export * from './bity';

364
common/config/networks.ts Normal file
View File

@ -0,0 +1,364 @@
import { ethPlorer, ETHTokenExplorer, SecureWalletName, InsecureWalletName } from './data';
import { EtherscanNode, InfuraNode, RPCNode, Web3Node } from 'libs/nodes';
import { networkIdToName } from 'libs/values';
import {
ETH_DEFAULT,
ETH_TREZOR,
ETH_LEDGER,
ETC_LEDGER,
ETC_TREZOR,
ETH_TESTNET,
EXP_DEFAULT,
UBQ_DEFAULT,
DPath
} from 'config/dpaths';
export interface BlockExplorerConfig {
origin: string;
txUrl(txHash: string): string;
addressUrl(address: string): string;
}
export interface Token {
address: string;
symbol: string;
decimal: number;
error?: string | null;
}
export interface NetworkContract {
name: NetworkKeys;
address?: string;
abi: string;
}
export interface DPathFormats {
trezor: DPath;
ledgerNanoS: DPath;
mnemonicPhrase: DPath;
}
export interface NetworkConfig {
// TODO really try not to allow strings due to custom networks
name: NetworkKeys;
unit: string;
color?: string;
blockExplorer?: BlockExplorerConfig;
tokenExplorer?: {
name: string;
address(address: string): string;
};
chainId: number;
tokens: Token[];
contracts: NetworkContract[] | null;
dPathFormats: DPathFormats;
isTestnet?: boolean;
}
export interface CustomNetworkConfig {
name: string;
unit: string;
chainId: number;
dPathFormats: DPathFormats | null;
}
export interface NodeConfig {
network: NetworkKeys;
lib: RPCNode | Web3Node;
service: string;
estimateGas?: boolean;
hidden?: boolean;
}
export interface CustomNodeConfig {
name: string;
url: string;
port: number;
network: string;
auth?: {
username: string;
password: string;
};
}
// Must be a website that follows the ethplorer convention of /tx/[hash] and
// address/[address] to generate the correct functions.
function makeExplorer(origin: string): BlockExplorerConfig {
return {
origin,
txUrl: hash => `${origin}/tx/${hash}`,
addressUrl: address => `${origin}/address/${address}`
};
}
const ETH: NetworkConfig = {
name: 'ETH',
unit: 'ETH',
chainId: 1,
color: '#0e97c0',
blockExplorer: makeExplorer('https://etherscan.io'),
tokenExplorer: {
name: ethPlorer,
address: ETHTokenExplorer
},
tokens: require('./tokens/eth.json'),
contracts: require('./contracts/eth.json'),
dPathFormats: {
[SecureWalletName.TREZOR]: ETH_TREZOR,
[SecureWalletName.LEDGER_NANO_S]: ETH_LEDGER,
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_DEFAULT
}
};
const Ropsten: NetworkConfig = {
name: 'Ropsten',
unit: 'ETH',
chainId: 3,
color: '#adc101',
blockExplorer: makeExplorer('https://ropsten.etherscan.io'),
tokens: require('./tokens/ropsten.json'),
contracts: require('./contracts/ropsten.json'),
isTestnet: true,
dPathFormats: {
[SecureWalletName.TREZOR]: ETH_TESTNET,
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
}
};
const Kovan: NetworkConfig = {
name: 'Kovan',
unit: 'ETH',
chainId: 42,
color: '#adc101',
blockExplorer: makeExplorer('https://kovan.etherscan.io'),
tokens: require('./tokens/ropsten.json'),
contracts: require('./contracts/ropsten.json'),
isTestnet: true,
dPathFormats: {
[SecureWalletName.TREZOR]: ETH_TESTNET,
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
}
};
const Rinkeby: NetworkConfig = {
name: 'Rinkeby',
unit: 'ETH',
chainId: 4,
color: '#adc101',
blockExplorer: makeExplorer('https://rinkeby.etherscan.io'),
tokens: require('./tokens/rinkeby.json'),
contracts: require('./contracts/rinkeby.json'),
isTestnet: true,
dPathFormats: {
[SecureWalletName.TREZOR]: ETH_TESTNET,
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
}
};
const ETC: NetworkConfig = {
name: 'ETC',
unit: 'ETC',
chainId: 61,
color: '#669073',
blockExplorer: makeExplorer('https://gastracker.io'),
tokens: require('./tokens/etc.json'),
contracts: require('./contracts/etc.json'),
dPathFormats: {
[SecureWalletName.TREZOR]: ETC_TREZOR,
[SecureWalletName.LEDGER_NANO_S]: ETC_LEDGER,
[InsecureWalletName.MNEMONIC_PHRASE]: ETC_TREZOR
}
};
const UBQ: NetworkConfig = {
name: 'UBQ',
unit: 'UBQ',
chainId: 8,
color: '#b37aff',
blockExplorer: makeExplorer('https://ubiqscan.io/en'),
tokens: require('./tokens/ubq.json'),
contracts: require('./contracts/ubq.json'),
dPathFormats: {
[SecureWalletName.TREZOR]: UBQ_DEFAULT,
[SecureWalletName.LEDGER_NANO_S]: UBQ_DEFAULT,
[InsecureWalletName.MNEMONIC_PHRASE]: UBQ_DEFAULT
}
};
const EXP: NetworkConfig = {
name: 'EXP',
unit: 'EXP',
chainId: 2,
color: '#673ab7',
blockExplorer: makeExplorer('http://www.gander.tech'),
tokens: require('./tokens/exp.json'),
contracts: require('./contracts/exp.json'),
dPathFormats: {
[SecureWalletName.TREZOR]: EXP_DEFAULT,
[SecureWalletName.LEDGER_NANO_S]: EXP_DEFAULT,
[InsecureWalletName.MNEMONIC_PHRASE]: EXP_DEFAULT
}
};
export const NETWORKS = {
ETH,
Ropsten,
Kovan,
Rinkeby,
ETC,
UBQ,
EXP
};
export type NetworkKeys = keyof typeof NETWORKS;
enum NodeName {
ETH_MEW = 'eth_mew',
ETH_ETHSCAN = 'eth_ethscan',
ETH_INFURA = 'eth_infura',
ROP_MEW = 'rop_mew',
ROP_INFURA = 'rop_infura',
KOV_ETHSCAN = 'kov_ethscan',
RIN_ETHSCAN = 'rin_ethscan',
RIN_INFURA = 'rin_infura',
ETC_EPOOL = 'etc_epool',
UBQ = 'ubq',
EXP_TECH = 'exp_tech'
}
type NonWeb3NodeConfigs = { [key in NodeName]: NodeConfig };
interface Web3NodeConfig {
web3?: NodeConfig;
}
type NodeConfigs = NonWeb3NodeConfigs & Web3NodeConfig;
export const NODES: NodeConfigs = {
eth_mew: {
network: 'ETH',
lib: new RPCNode('https://api.myetherapi.com/eth'),
service: 'MyEtherWallet',
estimateGas: true
},
eth_ethscan: {
network: 'ETH',
service: 'Etherscan.io',
lib: new EtherscanNode('https://api.etherscan.io/api'),
estimateGas: false
},
eth_infura: {
network: 'ETH',
service: 'infura.io',
lib: new InfuraNode('https://mainnet.infura.io/mew'),
estimateGas: false
},
rop_mew: {
network: 'Ropsten',
service: 'MyEtherWallet',
lib: new RPCNode('https://api.myetherapi.com/rop'),
estimateGas: false
},
rop_infura: {
network: 'Ropsten',
service: 'infura.io',
lib: new InfuraNode('https://ropsten.infura.io/mew'),
estimateGas: false
},
kov_ethscan: {
network: 'Kovan',
service: 'Etherscan.io',
lib: new EtherscanNode('https://kovan.etherscan.io/api'),
estimateGas: false
},
rin_ethscan: {
network: 'Rinkeby',
service: 'Etherscan.io',
lib: new EtherscanNode('https://rinkeby.etherscan.io/api'),
estimateGas: false
},
rin_infura: {
network: 'Rinkeby',
service: 'infura.io',
lib: new InfuraNode('https://rinkeby.infura.io/mew'),
estimateGas: false
},
etc_epool: {
network: 'ETC',
service: 'Epool.io',
lib: new RPCNode('https://mewapi.epool.io'),
estimateGas: false
},
ubq: {
network: 'UBQ',
service: 'ubiqscan.io',
lib: new RPCNode('https://pyrus2.ubiqscan.io'),
estimateGas: true
},
exp_tech: {
network: 'EXP',
service: 'Expanse.tech',
lib: new RPCNode('https://node.expanse.tech/'),
estimateGas: true
}
};
interface Web3NodeInfo {
networkId: string;
lib: Web3Node;
}
export async function setupWeb3Node(): Promise<Web3NodeInfo> {
const { web3 } = window as any;
if (!web3 || !web3.currentProvider || !web3.currentProvider.sendAsync) {
throw new Error(
'Web3 not found. Please check that MetaMask is installed, or that MyEtherWallet is open in Mist.'
);
}
const lib = new Web3Node();
const networkId = await lib.getNetVersion();
const accounts = await lib.getAccounts();
if (!accounts.length) {
throw new Error('No accounts found in MetaMask / Mist.');
}
if (networkId === 'loading') {
throw new Error('MetaMask / Mist is still loading. Please refresh the page and try again.');
}
return { networkId, lib };
}
export async function isWeb3NodeAvailable(): Promise<boolean> {
try {
await setupWeb3Node();
return true;
} catch (e) {
return false;
}
}
export const Web3Service = 'MetaMask / Mist';
export interface NodeConfigOverride extends NodeConfig {
network: any;
}
export async function initWeb3Node(): Promise<void> {
const { networkId, lib } = await setupWeb3Node();
const web3: NodeConfigOverride = {
network: networkIdToName(networkId),
service: Web3Service,
lib,
estimateGas: false,
hidden: true
};
NODES.web3 = web3;
}

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import translate from 'translations';
import { NetworkContract } from 'config/data';
import { NetworkContract } from 'config';
import { getNetworkContracts } from 'selectors/config';
import { connect } from 'react-redux';
import { AppState } from 'reducers';

View File

@ -1,7 +1,7 @@
import { NewTabLink } from 'components/ui';
import React from 'react';
import GeneralInfoNode from './GeneralInfoNode';
import { knowledgeBaseURL } from 'config/data';
import { knowledgeBaseURL } from 'config';
import { InfoNode } from './types';
const generalInfoNodes: InfoNode[] = [

View File

@ -5,7 +5,7 @@ import translate from 'translations';
import { makeBlob } from 'utils/blob';
import './DownloadWallet.scss';
import Template from '../Template';
import { N_FACTOR } from 'config/data';
import { N_FACTOR } from 'config';
interface Props {
wallet: IFullWallet;

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import translate from 'translations';
import { MINIMUM_PASSWORD_LENGTH } from 'config/data';
import { MINIMUM_PASSWORD_LENGTH } from 'config';
import './EnterPassword.scss';
import PasswordInput from './PasswordInput';
import Template from '../Template';

View File

@ -1,7 +1,7 @@
import React from 'react';
import translate from 'translations';
import TabSection from 'containers/TabSection';
import { knowledgeBaseURL } from 'config/data';
import { knowledgeBaseURL } from 'config';
const Help = () => (
<TabSection>

View File

@ -7,7 +7,7 @@ import { isValidPrivKey } from 'libs/validators';
import { stripHexPrefix } from 'libs/values';
import translate from 'translations';
import './KeystoreDetails.scss';
import { N_FACTOR } from 'config/data';
import { N_FACTOR } from 'config';
interface State {
secretKey: string;

View File

@ -1,4 +1,4 @@
import { donationAddressMap } from 'config/data';
import { donationAddressMap } from 'config';
import React from 'react';
import translate from 'translations';

View File

@ -12,7 +12,7 @@ import {
ICurrentValue
} from 'selectors/transaction/current';
import BN from 'bn.js';
import { NetworkConfig } from 'config/data';
import { NetworkConfig } from 'config';
import { validNumber, validDecimal } from 'libs/validators';
import { getGasLimit } from 'selectors/transaction';
import { AddressField, AmountField, GasLimitField } from 'components';

View File

@ -3,24 +3,28 @@ import { connect } from 'react-redux';
import translate from 'translations';
import TabSection from 'containers/TabSection';
import { UnlockHeader } from 'components/ui';
import { SideBar } from './components/index';
import { SideBar } from './components';
import { IReadOnlyWallet, IFullWallet } from 'libs/wallet';
import { getWalletInst } from 'selectors/wallet';
import { AppState } from 'reducers';
import tabs from './tabs';
import SubTabs, { Props as TabProps } from 'components/SubTabs';
import { RouteComponentProps } from 'react-router';
import { NetworkConfig } from 'config';
import { getNetworkConfig } from 'selectors/config';
export type WalletTypes = IReadOnlyWallet | IFullWallet | undefined | null;
interface StateProps {
wallet: AppState['wallet']['inst'];
network: AppState['config']['network'];
}
export interface SubTabProps {
wallet: WalletTypes;
network: NetworkConfig;
}
export type WalletTypes = IReadOnlyWallet | IFullWallet | undefined | null;
type Props = StateProps & RouteComponentProps<{}>;
const determineActiveTab = (wallet: WalletTypes, activeTab: string) => {
@ -33,7 +37,7 @@ const determineActiveTab = (wallet: WalletTypes, activeTab: string) => {
class SendTransaction extends React.Component<Props> {
public render() {
const { wallet, location } = this.props;
const { wallet, location, network } = this.props;
const activeTab = location.pathname.split('/')[2];
const tabProps: TabProps<SubTabProps> = {
@ -41,7 +45,7 @@ class SendTransaction extends React.Component<Props> {
activeTab: determineActiveTab(wallet, activeTab),
sideBar: <SideBar />,
tabs,
subTabProps: { wallet }
subTabProps: { wallet, network }
};
interface IWalletTabs {
@ -61,4 +65,7 @@ class SendTransaction extends React.Component<Props> {
}
}
export default connect((state: AppState) => ({ wallet: getWalletInst(state) }))(SendTransaction);
export default connect((state: AppState) => ({
wallet: getWalletInst(state),
network: getNetworkConfig(state)
}))(SendTransaction);

View File

@ -5,6 +5,7 @@ import translate from 'translations';
import { Fields, UnavailableWallets, WalletInfo, RequestPayment } from './components/index';
import { Tab } from 'components/SubTabs';
import { SubTabProps } from 'containers/Tabs/SendTransaction';
import { isNetworkUnit } from 'utils/network';
const SendTab: Tab<SubTabProps> = {
path: 'send',
@ -41,6 +42,10 @@ const InfoTab: Tab<SubTabProps> = {
const RequestTab: Tab<SubTabProps> = {
path: 'request',
name: translate('Request Payment'),
isDisabled: (props: SubTabProps) => {
const isETHNetwork = isNetworkUnit(props.network, 'ETH');
return !isETHNetwork;
},
render(props: SubTabProps) {
const content = props && props.wallet ? <RequestPayment wallet={props.wallet} /> : null;
return <div>{content}</div>;

View File

@ -6,7 +6,7 @@ import {
SwapInput
} from 'reducers/swap/types';
import SimpleButton from 'components/ui/SimpleButton';
import bityConfig, { generateKindMax, generateKindMin, WhitelistedCoins } from 'config/bity';
import { generateKindMax, generateKindMin, WhitelistedCoins, bityConfig } from 'config/bity';
import React, { Component } from 'react';
import translate from 'translations';
import { combineAndUpper } from 'utils/formatters';

View File

@ -6,7 +6,7 @@ import {
import bityLogoWhite from 'assets/images/logo-bity-white.svg';
import shapeshiftLogoWhite from 'assets/images/logo-shapeshift.svg';
import Spinner from 'components/ui/Spinner';
import { bityReferralURL, shapeshiftReferralURL } from 'config/data';
import { bityReferralURL, shapeshiftReferralURL } from 'config';
import React, { Component } from 'react';
import translate from 'translations';
import './CurrentRates.scss';

View File

@ -8,7 +8,7 @@ import { configureLiteSend, TConfigureLiteSend } from 'actions/swap';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { shouldDisplayLiteSend } from 'selectors/swap';
import { NetworkConfig } from 'config/data';
import { NetworkConfig } from 'config';
interface DispatchProps {
configureLiteSend: TConfigureLiteSend;
@ -36,10 +36,7 @@ class LiteSendClass extends Component<Props> {
renderMe = (
<div className="row">
<div className="col-xs-8 col-xs-push-2 text-center">
<h5 style={{ color: 'red' }}>
WARNING: You are currently not on the Ethereum Mainnet. Please switch nodes in order
for the token swap to function as intended.
</h5>
<h5>Note: Send is only supported on Ethereum Mainnet.</h5>
</div>
</div>
);

View File

@ -8,7 +8,7 @@ import {
import { SwapInput } from 'reducers/swap/types';
import classnames from 'classnames';
import SimpleButton from 'components/ui/SimpleButton';
import { donationAddressMap } from 'config/data';
import { donationAddressMap } from 'config';
import { isValidBTCAddress, isValidETHAddress } from 'libs/validators';
import React, { Component } from 'react';
import translate from 'translations';

View File

@ -1,7 +1,7 @@
import { RestartSwapAction } from 'actions/swap';
import bityLogo from 'assets/images/logo-bity.svg';
import shapeshiftLogo from 'assets/images/shapeshift-dark.svg';
import { bityReferralURL } from 'config/data';
import { bityReferralURL } from 'config';
import React, { Component } from 'react';
import translate from 'translations';
import './SwapInfoHeader.scss';

View File

@ -1,5 +1,5 @@
import { TShowNotification } from 'actions/notifications';
import bityConfig from 'config/bity';
import { bityConfig } from 'config/bity';
import React, { Component } from 'react';
import translate, { translateRaw } from 'translations';
import './SwapProgress.scss';

View File

@ -1,4 +1,4 @@
import { Token } from 'config/data';
import { Token } from 'config';
import { Wei, TokenValue } from 'libs/units';
import { IHexStrTransaction } from 'libs/transaction';

View File

@ -1,6 +1,6 @@
import RPCNode from '../rpc';
import RPCClient from '../rpc/client';
import { CustomNodeConfig } from 'config/data';
import { CustomNodeConfig } from 'config';
export default class CustomNode extends RPCNode {
constructor(config: CustomNodeConfig) {

View File

@ -1,4 +1,4 @@
import { Token } from 'config/data';
import { Token } from 'config';
import ERC20 from 'libs/erc20';
import RPCRequests from '../rpc/requests';
import {

View File

@ -1,5 +1,5 @@
import BN from 'bn.js';
import { Token } from 'config/data';
import { Token } from 'config';
import { IHexStrTransaction } from 'libs/transaction';
import { Wei, TokenValue } from 'libs/units';
import { stripHexPrefix } from 'libs/values';

View File

@ -1,4 +1,4 @@
import { Token } from 'config/data';
import { Token } from 'config';
import ERC20 from 'libs/erc20';
import {
CallRequest,

View File

@ -1,6 +1,7 @@
import { Wei, toTokenBase } from 'libs/units';
import { addHexPrefix } from 'ethereumjs-util';
import BN from 'bn.js';
import { NetworkKeys } from 'config';
export function stripHexPrefix(value: string) {
return value.replace('0x', '');
@ -23,7 +24,7 @@ export function sanitizeHex(hex: string) {
return hex !== '' ? `0x${padLeftEven(hexStr)}` : '';
}
export function networkIdToName(networkId: string | number): string {
export function networkIdToName(networkId: string | number): NetworkKeys {
switch (networkId.toString()) {
case '1':
return 'ETH';

View File

@ -16,7 +16,7 @@ import {
CustomNodeConfig,
NetworkConfig,
CustomNetworkConfig
} from '../config/data';
} from 'config';
import { makeCustomNodeId } from 'utils/node';
import { makeCustomNetworkId } from 'utils/network';

View File

@ -4,7 +4,7 @@ import {
RemoveCustomTokenAction
} from 'actions/customTokens';
import { TypeKeys } from 'actions/customTokens/constants';
import { Token } from 'config/data';
import { Token } from 'config';
export type State = Token[];

View File

@ -1,5 +1,5 @@
import { Option } from 'actions/swap/actionTypes';
import { WhitelistedCoins } from 'config/bity';
import { WhitelistedCoins } from 'config';
export interface SwapInput {
id: WhitelistedCoins;

View File

@ -10,7 +10,14 @@ import {
select,
race
} from 'redux-saga/effects';
import { NODES, NETWORKS, NodeConfig, CustomNodeConfig, CustomNetworkConfig } from 'config/data';
import {
NODES,
NETWORKS,
NodeConfig,
CustomNodeConfig,
CustomNetworkConfig,
Web3Service
} from 'config';
import {
makeCustomNodeId,
getCustomNodeConfigFromId,
@ -37,12 +44,9 @@ import {
} from 'actions/config';
import { showNotification } from 'actions/notifications';
import { translateRaw } from 'translations';
import { IWallet, Web3Wallet } from 'libs/wallet';
import { getWalletInst } from 'selectors/wallet';
import { Web3Wallet } from 'libs/wallet';
import { TypeKeys as WalletTypeKeys } from 'actions/wallet/constants';
import { State as ConfigState, INITIAL_STATE as configInitialState } from 'reducers/config';
import { resetWallet } from 'actions/wallet';
import { reset as resetTransaction } from 'actions/transaction';
export const getConfig = (state: AppState): ConfigState => state.config;
@ -171,12 +175,18 @@ export function* handleNodeChangeIntent(action: ChangeNodeIntentAction): SagaIte
yield put(setLatestBlock(latestBlock));
yield put(changeNode(action.payload, actionConfig, actionNetwork));
const currentWallet: IWallet | null = yield select(getWalletInst);
// TODO - re-enable once DeterministicWallet state is fixed to flush properly.
// DeterministicWallet keeps path related state we need to flush before we can stop reloading
// const currentWallet: IWallet | null = yield select(getWalletInst);
// if there's no wallet, do not reload as there's no component state to resync
if (currentWallet && currentConfig.network !== actionConfig.network) {
yield put(resetWallet());
yield put(resetTransaction());
// if (currentWallet && currentConfig.network !== actionConfig.network) {
const isNewNetwork = currentConfig.network !== actionConfig.network;
const newIsWeb3 = actionConfig.service === Web3Service;
// don't reload when web3 is selected; node will automatically re-set and state is not an issue here
if (isNewNetwork && !newIsWeb3) {
yield call(reload);
}
}

View File

@ -5,7 +5,7 @@ import {
updateDeterministicWallet
} from 'actions/deterministicWallets';
import { showNotification } from 'actions/notifications';
import { Token } from 'config/data';
import { Token } from 'config';
import { publicToAddress, toChecksumAddress } from 'ethereumjs-util';
import HDKey from 'hdkey';
import { INode } from 'libs/nodes/INode';

View File

@ -12,7 +12,7 @@ import {
TypeKeys as TK
} from 'actions/transaction';
import Tx from 'ethereumjs-tx';
import { NetworkConfig } from 'config/data';
import { NetworkConfig } from 'config';
import { SagaIterator } from 'redux-saga';
import { showNotification } from 'actions/notifications';

View File

@ -1,6 +1,6 @@
import { apply, select, call } from 'redux-saga/effects';
import { AppState } from 'reducers';
import { Token } from 'config/data';
import { Token } from 'config';
import { INode } from 'libs/nodes/INode';
import { IWallet, WalletConfig } from 'libs/wallet';
import { TokenBalance } from 'selectors/wallet';

View File

@ -36,7 +36,7 @@ import {
Web3Wallet,
WalletConfig
} from 'libs/wallet';
import { NODES, initWeb3Node, Token } from 'config/data';
import { NODES, initWeb3Node, Token } from 'config';
import { SagaIterator, delay, Task } from 'redux-saga';
import { apply, call, fork, put, select, takeEvery, take, cancel } from 'redux-saga/effects';
import { getNodeLib, getAllTokens, getOffline } from 'selectors/config';
@ -261,6 +261,9 @@ export function* unlockWeb3(): SagaIterator {
action.type === ConfigTypeKeys.CONFIG_NODE_CHANGE && action.payload.nodeSelection === 'web3'
);
if (!NODES.web3) {
throw Error('Web3 node config not found!');
}
const network = NODES.web3.network;
const nodeLib: INode | Web3Node = yield select(getNodeLib);

View File

@ -24,6 +24,9 @@
// --- RC SLIDER ---
@import "~rc-slider/assets/index.css";
// --- React Select ---
@import '~react-select/dist/react-select.css';
// --- CUSTOM ---
@import "./styles/badbrowser";
@import "./styles/noscript";

View File

@ -1,11 +1,11 @@
import {
CustomNetworkConfig,
CustomNodeConfig,
NetworkConfig,
NetworkContract,
NodeConfig,
CustomNodeConfig,
CustomNetworkConfig,
Token
} from 'config/data';
} from 'config';
import { INode } from 'libs/nodes/INode';
import { AppState } from 'reducers';
import { getUnit } from 'selectors/transaction/meta';

View File

@ -1,5 +1,5 @@
import { TokenValue, Wei } from 'libs/units';
import { Token } from 'config/data';
import { Token } from 'config';
import { AppState } from 'reducers';
import { getNetworkConfig } from 'selectors/config';
import { IWallet, Web3Wallet, LedgerWallet, TrezorWallet, WalletConfig } from 'libs/wallet';

View File

@ -22,3 +22,6 @@ export function getParam(query: { [key: string]: string }, key: string) {
export function isPositiveInteger(n: number) {
return Number.isInteger(n) && n > 0;
}
export const getValues = (...args) =>
args.reduce((acc, currArg) => [...acc, ...Object.values(currArg)], []);

View File

@ -2,7 +2,7 @@ import { fromPrivateKey, IFullWallet, fromV3 } from 'ethereumjs-wallet';
import { isValidPrivKey } from 'libs/validators';
import { stripHexPrefix } from 'libs/values';
import { makeBlob } from 'utils/blob';
import { N_FACTOR } from 'config/data';
import { N_FACTOR } from 'config';
export interface KeystoreFile {
filename: string;

View File

@ -1,16 +1,43 @@
import { NETWORKS, NetworkConfig, CustomNetworkConfig } from 'config/data';
import {
CustomNetworkConfig,
DPathFormats,
InsecureWalletName,
NetworkConfig,
NETWORKS,
SecureWalletName,
WalletName,
walletNames
} from 'config';
import { DPath, EXTRA_PATHS } from 'config/dpaths';
import sortedUniq from 'lodash/sortedUniq';
import difference from 'lodash/difference';
export function makeCustomNetworkId(config: CustomNetworkConfig): string {
return config.chainId ? `${config.chainId}` : `${config.name}:${config.unit}`;
}
export function makeNetworkConfigFromCustomConfig(config: CustomNetworkConfig): NetworkConfig {
return {
// TODO - re-enable this block and classify customConfig after user-inputted dPaths are implemented
// -------------------------------------------------
// this still provides the type safety we want
// as we know config coming in is CustomNetworkConfig
// meaning name will be a string
// then we cast it as any to keep it as a network key
// interface Override extends NetworkConfig {
// name: any;
// }
// -------------------------------------------------
// TODO - allow for user-inputted dPaths so we don't need to use any below and can use supplied dPaths
// In the meantime, networks with an unknown chainId will have HD wallets disabled
const customConfig: any = {
...config,
color: '#000',
tokens: [],
contracts: []
};
return customConfig;
}
export function getNetworkConfigFromId(
@ -26,3 +53,68 @@ export function getNetworkConfigFromId(
return makeNetworkConfigFromCustomConfig(customConfig);
}
}
type PathType = keyof DPathFormats;
type DPathFormat =
| SecureWalletName.TREZOR
| SecureWalletName.LEDGER_NANO_S
| InsecureWalletName.MNEMONIC_PHRASE;
export function getPaths(pathType: PathType): DPath[] {
const networkPaths: DPath[] = [];
Object.values(NETWORKS).forEach(networkConfig => {
const path = networkConfig.dPathFormats ? networkConfig.dPathFormats[pathType] : [];
if (path) {
networkPaths.push(path as DPath);
}
});
const paths = networkPaths.concat(EXTRA_PATHS);
return sortedUniq(paths);
}
export function getSingleDPath(format: DPathFormat, network: NetworkConfig): DPath {
const dPathFormats = network.dPathFormats;
return dPathFormats[format];
}
export function isNetworkUnit(network: NetworkConfig, unit: string) {
const validNetworks = Object.values(NETWORKS).filter((n: NetworkConfig) => n.unit === unit);
return validNetworks.includes(network);
}
export function isWalletFormatSupportedOnNetwork(
format: WalletName,
network: NetworkConfig
): boolean {
const CHECK_FORMATS: DPathFormat[] = [
SecureWalletName.LEDGER_NANO_S,
SecureWalletName.TREZOR,
InsecureWalletName.MNEMONIC_PHRASE
];
const isHDFormat = (f: string): f is DPathFormat => CHECK_FORMATS.includes(f as DPathFormat);
// Ensure DPath's are found
if (isHDFormat(format)) {
const dPath = network.dPathFormats && network.dPathFormats[format];
return !!dPath;
}
// Ensure Web3 is only enabled on ETH or ETH Testnets (MetaMask does not support other networks)
if (format === SecureWalletName.WEB3) {
return isNetworkUnit(network, 'ETH');
}
// All other wallet formats are supported
return true;
}
export function allWalletFormatsSupportedOnNetwork(network: NetworkConfig): WalletName[] {
return walletNames.filter(walletName => isWalletFormatSupportedOnNetwork(walletName, network));
}
export function unSupportedWalletFormatsOnNetwork(network: NetworkConfig): WalletName[] {
const supportedFormats = allWalletFormatsSupportedOnNetwork(network);
return difference(walletNames, supportedFormats);
}

View File

@ -1,5 +1,5 @@
import { CustomNode } from 'libs/nodes';
import { NODES, NodeConfig, CustomNodeConfig } from 'config/data';
import { NODES, NodeConfig, CustomNodeConfig } from 'config';
export function makeCustomNodeId(config: CustomNodeConfig): string {
return `${config.url}:${config.port}`;
@ -27,10 +27,16 @@ export function getNodeConfigFromId(
}
export function makeNodeConfigFromCustomConfig(config: CustomNodeConfig): NodeConfig {
return {
interface Override extends NodeConfig {
network: any;
}
const customConfig: Override = {
network: config.network,
lib: new CustomNode(config),
service: 'your custom node',
estimateGas: true
};
return customConfig;
}

View File

@ -1,4 +1,4 @@
import { Token } from 'config/data';
import { Token } from 'config';
export function dedupeCustomTokens(networkTokens: Token[], customTokens: Token[]): Token[] {
if (!customTokens.length) {

View File

@ -37,6 +37,7 @@
"react-redux": "5.0.6",
"react-router-dom": "4.2.2",
"react-router-redux": "4.0.8",
"react-select": "1.2.1",
"react-stepper-horizontal": "1.0.9",
"react-transition-group": "2.2.1",
"redux": "3.7.2",
@ -62,6 +63,7 @@
"@types/react-redux": "5.0.14",
"@types/react-router-dom": "4.2.3",
"@types/react-router-redux": "5.0.11",
"@types/react-select": "1.1.0",
"@types/redux-logger": "3.0.5",
"@types/redux-promise-middleware": "0.0.9",
"@types/uuid": "3.4.3",

View File

@ -0,0 +1,19 @@
import { NETWORKS, NetworkConfig } from 'config';
describe('Networks', () => {
Object.keys(NETWORKS).forEach(networkId => {
it(`${networkId} contains non-null dPathFormats`, () => {
const network: NetworkConfig = NETWORKS[networkId];
Object.values(network.dPathFormats).forEach(dPathFormat => {
expect(dPathFormat).toBeTruthy();
});
});
});
it(`contain unique chainIds`, () => {
const networkValues = Object.values(NETWORKS);
const chainIds = networkValues.map(a => a.chainId);
const chainIdsSet = new Set(chainIds);
expect(Array.from(chainIdsSet).length).toEqual(chainIds.length);
});
});

View File

@ -1,4 +1,4 @@
import { NODES, NodeConfig } from '../../common/config/data';
import { NODES, NodeConfig } from 'config';
import { RPCNode } from '../../common/libs/nodes';
import { Validator } from 'jsonschema';
import { schema } from '../../common/libs/validators';

View File

@ -4,7 +4,7 @@ import Adapter from 'enzyme-adapter-react-16';
import SendTransaction from 'containers/Tabs/SendTransaction';
import shallowWithStore from '../utils/shallowWithStore';
import { createMockStore } from 'redux-test-utils';
import { NODES } from 'config/data';
import { NODES } from 'config';
import { RouteComponentProps } from 'react-router';
import { createMockRouteComponentProps } from '../utils/mockRouteComponentProps';

View File

@ -1,6 +1,6 @@
import { config, INITIAL_STATE } from 'reducers/config';
import * as configActions from 'actions/config';
import { NODES, NETWORKS } from 'config/data';
import { NODES, NETWORKS } from 'config';
import { makeCustomNodeId, makeNodeConfigFromCustomConfig } from 'utils/node';
const custNode = {

View File

@ -1,5 +1,5 @@
import { customTokens } from 'reducers/customTokens';
import { Token } from 'config/data';
import { Token } from 'config';
import * as customTokensActions from 'actions/customTokens';
describe('customTokens reducer', () => {

View File

@ -9,9 +9,10 @@ import {
handleNodeChangeIntent,
unsetWeb3Node,
unsetWeb3NodeOnWalletEvent,
equivalentNodeOrDefault
equivalentNodeOrDefault,
reload
} from 'sagas/config';
import { NODES, NodeConfig, NETWORKS } from 'config/data';
import { NODES, NodeConfig, NETWORKS } from 'config';
import {
getNode,
getNodeConfig,
@ -20,13 +21,10 @@ import {
getCustomNetworkConfigs
} from 'selectors/config';
import { INITIAL_STATE as configInitialState } from 'reducers/config';
import { getWalletInst } from 'selectors/wallet';
import { Web3Wallet } from 'libs/wallet';
import { RPCNode } from 'libs/nodes';
import { showNotification } from 'actions/notifications';
import { translateRaw } from 'translations';
import { resetWallet } from 'actions/wallet';
import { reset as resetTransaction } from 'actions/transaction';
// init module
configuredStore.getState();
@ -143,12 +141,12 @@ describe('handleNodeChangeIntent*', () => {
const customNetworkConfigs = [];
const defaultNodeNetwork = NETWORKS[defaultNodeConfig.network];
const newNode = Object.keys(NODES).reduce(
(acc, cur) => (NODES[acc].network === defaultNodeConfig.network ? cur : acc)
(acc, cur) => (NODES[cur].network !== defaultNodeConfig.network ? cur : acc)
);
const newNodeConfig = NODES[newNode];
const newNodeNetwork = NETWORKS[newNodeConfig.network];
const changeNodeIntentAction = changeNodeIntent(newNode);
const truthyWallet = true;
const latestBlock = '0xa';
const raceSuccess = {
lb: latestBlock
@ -208,15 +206,9 @@ describe('handleNodeChangeIntent*', () => {
);
});
it('should select getWalletInst', () => {
expect(data.gen.next().value).toEqual(select(getWalletInst));
});
it('should call reload if wallet exists and network is new', () => {
data.clone2 = data.gen.clone();
expect(data.clone2.next(truthyWallet).value).toEqual(put(resetWallet()));
expect(data.clone2.next(truthyWallet).value).toEqual(put(resetTransaction()));
expect(data.clone2.next().done).toEqual(true);
it('should call reload if network is new', () => {
expect(data.gen.next().value).toEqual(call(reload));
expect(data.gen.next().done).toEqual(true);
});
it('should be done', () => {
@ -366,7 +358,7 @@ describe('equivalentNodeOrDefault', () => {
const node = equivalentNodeOrDefault({
...mockNodeConfig,
network: 'noEqivalentExists'
});
} as any);
expect(node).toEqual(appDefaultNode);
});
@ -374,12 +366,12 @@ describe('equivalentNodeOrDefault', () => {
NODES.web3 = {
...mockNodeConfig,
network: 'uniqueToWeb3'
};
} as any;
const node = equivalentNodeOrDefault({
...mockNodeConfig,
network: 'uniqueToWeb3'
});
} as any);
expect(node).toEqual(appDefaultNode);
});
});

View File

@ -7,7 +7,7 @@ import { getDesiredToken, getWallets } from 'selectors/deterministicWallets';
import { getTokens } from 'selectors/wallet';
import { getNodeLib } from 'selectors/config';
import * as dWalletActions from 'actions/deterministicWallets';
import { Token } from 'config/data';
import { Token } from 'config';
import {
getDeterministicWallets,
updateWalletValues,

View File

@ -13,7 +13,7 @@ import {
import { Wei } from 'libs/units';
import { changeNodeIntent, web3UnsetNode } from 'actions/config';
import { INode } from 'libs/nodes/INode';
import { initWeb3Node, Token, N_FACTOR } from 'config/data';
import { initWeb3Node, Token, N_FACTOR } from 'config';
import { apply, call, fork, put, select, take } from 'redux-saga/effects';
import { getNodeLib, getOffline } from 'selectors/config';
import { getWalletInst, getWalletConfigTokens } from 'selectors/wallet';