2017-11-18 13:33:53 -07:00
import React from 'react' ;
2018-06-17 20:53:00 -05:00
import { connect } from 'react-redux' ;
import { exists , SuccessConfig , FailConfig } from 'mycrypto-eth-exists' ;
2018-03-08 14:28:43 -05:00
import translate , { translateRaw } from 'translations' ;
2018-02-12 15:43:07 -05:00
import { CustomNetworkConfig } from 'types/network' ;
import { CustomNodeConfig } from 'types/node' ;
2018-06-17 20:53:00 -05:00
import { AppState } from 'features/reducers' ;
2018-02-12 15:43:07 -05:00
import {
getCustomNetworkConfigs ,
2018-06-17 20:53:00 -05:00
getStaticNetworkConfigs ,
2018-02-12 15:43:07 -05:00
getCustomNodeConfigs ,
2018-06-17 20:53:00 -05:00
AddCustomNodeAction ,
TAddCustomNetwork ,
addCustomNetwork
} from 'features/config' ;
2018-03-23 12:41:47 -04:00
import { Input , Dropdown } from 'components/ui' ;
2018-06-17 20:53:00 -05:00
import Modal , { IButton } from 'components/ui/Modal' ;
2018-03-08 14:28:43 -05:00
import './CustomNodeModal.scss' ;
2017-11-18 13:33:53 -07:00
2018-03-08 14:28:43 -05:00
const CUSTOM = { label : 'Custom' , value : 'custom' };
2017-11-18 13:33:53 -07:00
2018-02-12 15:43:07 -05:00
interface OwnProps {
2018-03-08 14:28:43 -05:00
isOpen : boolean ;
2018-02-12 15:43:07 -05:00
addCustomNode ( payload : AddCustomNodeAction [ 'payload' ]) : void ;
2017-11-18 13:33:53 -07:00
handleClose () : void ;
}
2018-02-12 15:43:07 -05:00
interface DispatchProps {
addCustomNetwork : TAddCustomNetwork ;
}
interface StateProps {
customNodes : AppState [ 'config' ][ 'nodes' ][ 'customNodes' ];
customNetworks : AppState [ 'config' ][ 'networks' ][ 'customNetworks' ];
staticNetworks : AppState [ 'config' ][ 'networks' ][ 'staticNetworks' ];
}
2017-11-18 13:33:53 -07:00
interface State {
name : string ;
url : string ;
network : string ;
2018-02-12 15:43:07 -05:00
customNetworkId : string ;
2017-12-01 08:09:51 -08:00
customNetworkUnit : string ;
customNetworkChainId : string ;
2017-11-18 13:33:53 -07:00
hasAuth : boolean ;
username : string ;
password : string ;
2018-06-12 13:01:14 -04:00
defaultNodes : (( SuccessConfig | FailConfig ) & { display : string ; index : number })[];
2017-11-18 13:33:53 -07:00
}
2018-02-12 15:43:07 -05:00
type Props = OwnProps & StateProps & DispatchProps ;
class CustomNodeModal extends React . Component < Props , State > {
2018-06-12 13:01:14 -04:00
public INITIAL_STATE : State = {
2017-11-18 13:33:53 -07:00
name : '' ,
url : '' ,
2018-02-12 15:43:07 -05:00
network : Object.keys ( this . props . staticNetworks )[ 0 ],
customNetworkId : '' ,
2017-12-01 08:09:51 -08:00
customNetworkUnit : '' ,
customNetworkChainId : '' ,
2017-11-18 13:33:53 -07:00
hasAuth : false ,
username : '' ,
2018-06-12 13:01:14 -04:00
password : '' ,
defaultNodes : []
2017-11-18 13:33:53 -07:00
};
2018-06-12 13:01:14 -04:00
2018-03-08 14:28:43 -05:00
public state : State = this . INITIAL_STATE ;
2018-06-12 13:01:14 -04:00
private timer : number | null ;
constructor ( props : Props ) {
super ( props );
this . pollForDefaultNodes ();
}
2018-03-08 14:28:43 -05:00
public componentDidUpdate ( prevProps : Props ) {
// Reset state when modal opens
if ( ! prevProps . isOpen && prevProps . isOpen !== this . props . isOpen ) {
this . setState ( this . INITIAL_STATE );
}
}
2017-11-18 13:33:53 -07:00
2018-06-12 13:01:14 -04:00
public componentWillUnmount() {
if ( this . timer ) {
window . clearInterval ( this . timer );
}
this . timer = null ;
}
2017-11-18 13:33:53 -07:00
public render() {
2018-03-08 14:28:43 -05:00
const { customNetworks , handleClose , staticNetworks , isOpen } = this . props ;
2018-05-29 10:51:42 -04:00
const { network , customNetworkChainId } = this . state ;
2017-11-18 13:33:53 -07:00
const isHttps = window . location . protocol . includes ( 'https' );
const invalids = this . getInvalids ();
2017-11-29 15:14:57 -08:00
const buttons : IButton [] = [
{
type : 'primary' ,
text : translate ( 'NODE_CTA' ),
onClick : this.saveAndAdd ,
disabled : !! Object . keys ( invalids ). length
},
{
2017-12-14 00:08:45 -05:00
type : 'default' ,
2018-03-21 23:50:25 -04:00
text : translate ( 'ACTION_2' ),
2017-11-29 15:14:57 -08:00
onClick : handleClose
}
];
2017-11-18 13:33:53 -07:00
2018-05-29 10:51:42 -04:00
const nameConflictNode = this . getNameConflictNode ();
const chainidConflictNetwork =
network === CUSTOM . value && this . getChainIdCollisionNetwork ( customNetworkChainId );
2018-03-08 14:28:43 -05:00
const staticNetwrks = Object . keys ( staticNetworks ). map ( net => {
return { label : net , value : net };
});
const customNetwrks = Object . entries ( customNetworks ). map (([ id , net ]) => {
return { label : net.name + ' (Custom)' , value : id };
});
const options = [... staticNetwrks , ... customNetwrks , CUSTOM ];
2017-11-18 13:33:53 -07:00
return (
< Modal
2018-04-13 00:36:51 -04:00
title = { translateRaw ( 'NODE_TITLE' )}
2018-03-08 14:28:43 -05:00
isOpen = { isOpen }
2017-11-18 13:33:53 -07:00
buttons = { buttons }
handleClose = { handleClose }
2018-02-24 13:02:07 -05:00
maxWidth = { 580 }
2017-11-18 13:33:53 -07:00
>
2018-03-21 23:50:25 -04:00
{ isHttps && < div className = "alert alert-warning small" >{ translate ( 'NODE_WARNING' )}</ div >}
2017-11-18 13:33:53 -07:00
2018-05-29 10:51:42 -04:00
{ nameConflictNode && (
2018-03-08 14:28:43 -05:00
< div className = "alert alert-warning small" >
2018-05-29 10:51:42 -04:00
{ translate ( 'CUSTOM_NODE_NAME_CONFLICT' , { $node : nameConflictNode.name })}
2018-03-08 14:28:43 -05:00
</ div >
)}
2018-06-12 13:01:14 -04:00
{ this . renderDefaultNodeDropdown ()}
2018-03-08 14:28:43 -05:00
< form className = "CustomNodeModal" >
< div className = "flex-wrapper" >
< label className = "col-sm-9 input-group flex-grow-1" >
2018-03-21 23:50:25 -04:00
< div className = "input-group-header" >{ translate ( 'CUSTOM_NODE_NAME' )}</ div >
2018-03-08 14:28:43 -05:00
< Input
2018-05-13 15:24:50 -04:00
isValid = { ! ( this . state . name && invalids . name )}
2018-03-08 14:28:43 -05:00
type = "text"
placeholder = "My Node"
value = { this . state . name }
onChange = { e => this . setState ({ name : e.currentTarget.value })}
/>
</ label >
< label className = "col-sm-3 input-group" >
2018-04-13 00:36:51 -04:00
< div className = "input-group-header" >{ translate ( 'CUSTOM_NETWORK' )}</ div >
2018-03-08 14:28:43 -05:00
< Dropdown
value = { network }
options = { options }
clearable = { false }
onChange = {( e : { label : string ; value : string }) =>
this . setState ({ network : e.value })
}
/>
</ label >
</ div >
{ network === CUSTOM . value && (
< div className = "flex-wrapper" >
< label className = "col-sm-6 input-group input-group-inline" >
2018-03-21 23:50:25 -04:00
< div className = "input-group-header" >{ translate ( 'CUSTOM_NETWORK_NAME' )}</ div >
2018-03-08 14:28:43 -05:00
< Input
2018-05-13 15:24:50 -04:00
isValid = { ! ( this . state . customNetworkId && invalids . customNetworkId )}
2018-03-08 14:28:43 -05:00
type = "text"
placeholder = "My Custom Network"
value = { this . state . customNetworkId }
onChange = { e => this . setState ({ customNetworkId : e.currentTarget.value })}
/>
</ label >
< label className = "col-sm-3 input-group input-group-inline" >
2018-03-21 23:50:25 -04:00
< div className = "input-group-header" >{ translate ( 'CUSTOM_NETWORK_CURRENCY' )}</ div >
2018-03-08 14:28:43 -05:00
< Input
2018-05-13 15:24:50 -04:00
isValid = { ! ( this . state . customNetworkUnit && invalids . customNetworkUnit )}
2018-03-08 14:28:43 -05:00
type = "text"
placeholder = "ETH"
value = { this . state . customNetworkUnit }
onChange = { e => this . setState ({ customNetworkUnit : e.currentTarget.value })}
/>
</ label >
< label className = "col-sm-3 input-group input-group-inline" >
2018-03-21 23:50:25 -04:00
< div className = "input-group-header" >{ translate ( 'CUSTOM_NETWORK_CHAIN_ID' )}</ div >
2018-03-08 14:28:43 -05:00
< Input
2018-05-13 15:24:50 -04:00
isValid = { ! ( this . state . customNetworkChainId && invalids . customNetworkChainId )}
2018-03-08 14:28:43 -05:00
type = "text"
placeholder = "1"
value = { this . state . customNetworkChainId }
onChange = { e => this . setState ({ customNetworkChainId : e.currentTarget.value })}
/>
</ label >
2017-12-01 08:09:51 -08:00
</ div >
)}
2018-05-29 10:51:42 -04:00
{ chainidConflictNetwork && (
< div className = "alert alert-warning small" >
{ translate ( 'CUSTOM_NODE_CHAINID_CONFLICT' , { $network : chainidConflictNetwork.name })}
</ div >
)}
2017-12-01 08:09:51 -08:00
2018-03-08 14:28:43 -05:00
< label className = "input-group input-group-inline" >
2018-03-21 23:50:25 -04:00
< div className = "input-group-header" >{ translate ( 'CUSTOM_NETWORK_URL' )}</ div >
2018-03-08 14:28:43 -05:00
< Input
2018-05-13 15:24:50 -04:00
isValid = { ! ( this . state . url && invalids . url )}
2018-03-08 14:28:43 -05:00
type = "text"
placeholder = "https://127.0.0.1:8545/"
value = { this . state . url }
onChange = { e => this . setState ({ url : e.currentTarget.value })}
autoComplete = "off"
/>
</ label >
2017-11-18 13:33:53 -07:00
2018-03-08 14:28:43 -05:00
< label >
< input
type = "checkbox"
name = "hasAuth"
checked = { this . state . hasAuth }
onChange = {() => this . setState ({ hasAuth : ! this . state . hasAuth })}
/>
2018-03-21 23:50:25 -04:00
< span >{ translate ( 'CUSTOM_NETWORK_HTTP_AUTH' )}</ span >
2018-03-08 14:28:43 -05:00
</ label >
2017-12-01 08:09:51 -08:00
2018-03-08 14:28:43 -05:00
{ this . state . hasAuth && (
< div className = "flex-wrapper " >
< label className = "col-sm-6 input-group input-group-inline" >
2018-03-21 23:50:25 -04:00
< div className = "input-group-header" >{ translate ( 'INPUT_USERNAME_LABEL' )}</ div >
2018-03-08 14:28:43 -05:00
< Input
2018-05-13 15:24:50 -04:00
isValid = { ! ( this . state . username && invalids . username )}
2018-03-08 14:28:43 -05:00
type = "text"
value = { this . state . username }
onChange = { e => this . setState ({ username : e.currentTarget.value })}
/>
</ label >
< label className = "col-sm-6 input-group input-group-inline" >
2018-03-21 23:50:25 -04:00
< div className = "input-group-header" >{ translate ( 'INPUT_PASSWORD_LABEL' )}</ div >
2018-03-08 14:28:43 -05:00
< Input
2018-05-13 15:24:50 -04:00
isValid = { ! ( this . state . password && invalids . password )}
2018-03-08 14:28:43 -05:00
type = "password"
value = { this . state . password }
onChange = { e => this . setState ({ password : e.currentTarget.value })}
/>
</ label >
2017-11-18 13:33:53 -07:00
</ div >
2018-03-08 14:28:43 -05:00
)}
</ form >
2017-11-18 13:33:53 -07:00
</ Modal >
2018-06-12 13:01:14 -04:00
);
}
private pollForDefaultNodes() {
2018-07-06 18:45:25 -05:00
return null ;
// @ts-ignore
2018-06-12 13:01:14 -04:00
const pollingInterval = 3000 ;
2018-07-06 18:45:25 -05:00
// console.warning in production to explain to users why we are making a call to localhost
console . warn (
"Don't panic! MyCrypto is going to start a poll for default nodes on port 8545. If you don't like this feature, send us a ping at support@mycrypto.com and we'll walk you through disabling it."
);
2018-06-12 13:01:14 -04:00
this . timer = window . setInterval ( async () => {
const results = await exists (
[
// tslint:disable-next-line:no-http-string
{ type : 'http' , addr : 'http://localhost' , port : 8545 , timeout : 3000 }
],
{ includeDefaults : false }
);
if ( ! this . timer ) {
return ;
}
this . setState ({
defaultNodes : results.filter ( r => r . success ). map (( r , index ) => ({
... r ,
display : ` ${ r . addr } : ${ r . port } ` ,
index
}))
});
}, pollingInterval );
}
private renderDefaultNodeDropdown() {
const { defaultNodes } = this . state ;
if ( ! defaultNodes . length ) {
return null ;
}
return (
< label className = "col-sm-12 input-group" >
< div className = "input-group-header" > { 'Default Nodes Found' }</ div >
< Dropdown
options = { this . state . defaultNodes . map ( n => ({ label : n.display , value : n.index }))}
onChange = {({ value } : { value : string }) => {
const result = this . state . defaultNodes . find ( d => d . index === + value );
if ( ! result ) {
return ;
}
const { addr , port } = result ;
this . setState ({ url : ` ${ addr } : ${ port } ` , name : 'MyDefaultNode' });
}}
/>
</ label >
2017-11-18 13:33:53 -07:00
);
}
private getInvalids () : { [ key : string ] : boolean } {
2017-12-01 08:09:51 -08:00
const {
url ,
hasAuth ,
username ,
password ,
network ,
2018-02-12 15:43:07 -05:00
customNetworkId ,
2017-12-01 08:09:51 -08:00
customNetworkUnit ,
customNetworkChainId
} = this . state ;
2018-02-24 13:02:07 -05:00
const required : ( keyof State )[] = [ 'name' , 'url' , 'network' ];
2017-11-18 13:33:53 -07:00
const invalids : { [ key : string ] : boolean } = {};
// Required fields
2017-11-29 15:14:57 -08:00
required . forEach ( field => {
2017-11-18 13:33:53 -07:00
if ( ! this . state [ field ]) {
invalids [ field ] = true ;
}
});
2018-02-24 13:02:07 -05:00
// Parse the URL, and make sure what they typed isn't parsed as relative.
// Not a perfect regex, just checks for protocol + any char
if ( ! /^https?:\/\/.+/i . test ( url )) {
2017-11-18 13:33:53 -07:00
invalids . url = true ;
}
// If they have auth, make sure it's provided
if ( hasAuth ) {
if ( ! username ) {
invalids . username = true ;
}
if ( ! password ) {
invalids . password = true ;
}
}
2017-12-01 08:09:51 -08:00
// If they have a custom network, make sure info is provided
2018-03-08 14:28:43 -05:00
if ( network === CUSTOM . value ) {
2018-02-12 15:43:07 -05:00
if ( ! customNetworkId ) {
invalids . customNetworkId = true ;
2017-12-01 08:09:51 -08:00
}
if ( ! customNetworkUnit ) {
invalids . customNetworkUnit = true ;
}
2018-05-29 10:51:42 -04:00
// Numeric chain ID
if ( this . getChainIdCollisionNetwork ( customNetworkChainId )) {
2017-12-01 08:09:51 -08:00
invalids . customNetworkChainId = true ;
2018-05-29 10:51:42 -04:00
} else {
const iChainId = parseInt ( customNetworkChainId , 10 );
if ( ! customNetworkChainId || ! iChainId || iChainId < 0 ) {
invalids . customNetworkChainId = true ;
}
2017-12-01 08:09:51 -08:00
}
}
2017-11-18 13:33:53 -07:00
return invalids ;
}
2018-05-29 10:51:42 -04:00
private getChainIdCollisionNetwork ( chainId : string ) {
if ( ! chainId ) {
return false ;
}
const chainIdInt = parseInt ( chainId , 10 );
const allNetworks = [
... Object . values ( this . props . staticNetworks ),
... Object . values ( this . props . customNetworks )
];
return allNetworks . reduce (
( collision , network ) => ( network . chainId === chainIdInt ? network : collision ),
null
);
}
2017-12-01 08:09:51 -08:00
private makeCustomNetworkConfigFromState () : CustomNetworkConfig {
2018-02-12 15:43:07 -05:00
const similarNetworkConfig = Object . values ( this . props . staticNetworks ). find (
2018-01-20 14:06:28 -06:00
n => n . chainId === + this . state . customNetworkChainId
);
const dPathFormats = similarNetworkConfig ? similarNetworkConfig.dPathFormats : null ;
2017-12-01 08:09:51 -08:00
return {
2018-02-12 15:43:07 -05:00
isCustom : true ,
2018-05-29 10:51:42 -04:00
id : this.state.customNetworkChainId ,
2018-02-12 15:43:07 -05:00
name : this.state.customNetworkId ,
2017-12-01 08:09:51 -08:00
unit : this.state.customNetworkUnit ,
2018-05-29 10:51:42 -04:00
chainId : parseInt ( this . state . customNetworkChainId , 10 ),
2018-01-20 14:06:28 -06:00
dPathFormats
2017-12-01 08:09:51 -08:00
};
}
private makeCustomNodeConfigFromState () : CustomNodeConfig {
2018-02-24 13:02:07 -05:00
const { network , url , name , username , password } = this . state ;
2018-02-12 15:43:07 -05:00
const networkId =
2018-03-08 14:28:43 -05:00
network === CUSTOM . value
2018-02-12 15:43:07 -05:00
? this . makeCustomNetworkId ( this . makeCustomNetworkConfigFromState ())
: network ;
2018-05-29 10:51:42 -04:00
return {
2018-02-12 15:43:07 -05:00
isCustom : true ,
service : 'your custom node' ,
2018-02-24 13:02:07 -05:00
id : url ,
name : name.trim (),
2018-02-12 15:43:07 -05:00
url ,
network : networkId ,
...( this . state . hasAuth
? {
auth : {
2018-02-24 13:02:07 -05:00
username ,
password
2018-02-12 15:43:07 -05:00
}
}
: {})
2017-12-01 08:09:51 -08:00
};
}
2018-05-29 10:51:42 -04:00
private getNameConflictNode () : CustomNodeConfig | undefined {
2017-12-01 08:09:51 -08:00
const { customNodes } = this . props ;
const config = this . makeCustomNodeConfigFromState ();
2018-02-12 15:43:07 -05:00
return customNodes [ config . id ];
2017-12-01 08:09:51 -08:00
}
2017-11-18 13:33:53 -07:00
private saveAndAdd = () => {
2017-12-01 08:09:51 -08:00
const node = this . makeCustomNodeConfigFromState ();
2017-11-18 13:33:53 -07:00
2018-03-08 14:28:43 -05:00
if ( this . state . network === CUSTOM . value ) {
2017-12-01 08:09:51 -08:00
const network = this . makeCustomNetworkConfigFromState ();
2018-01-20 14:06:28 -06:00
2018-05-29 10:51:42 -04:00
this . props . addCustomNetwork ( network );
2017-11-18 13:33:53 -07:00
}
2018-05-29 10:51:42 -04:00
this . props . addCustomNode ( node );
2017-11-18 13:33:53 -07:00
};
2018-02-12 15:43:07 -05:00
private makeCustomNetworkId ( config : CustomNetworkConfig ) : string {
2018-05-29 10:51:42 -04:00
return config . chainId . toString ();
2018-02-12 15:43:07 -05:00
}
2017-11-18 13:33:53 -07:00
}
2018-02-12 15:43:07 -05:00
const mapStateToProps = ( state : AppState ) : StateProps => ({
customNetworks : getCustomNetworkConfigs ( state ),
customNodes : getCustomNodeConfigs ( state ),
staticNetworks : getStaticNetworkConfigs ( state )
});
const mapDispatchToProps : DispatchProps = {
addCustomNetwork
};
export default connect ( mapStateToProps , mapDispatchToProps )( CustomNodeModal );