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 > }
2018-03-08 14:28:43 -05: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 >
< 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 >
{ 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 ) ;