Fix Duplicate DPaths & Deterministic Modal Improvements (#1682)

* Fix dPath uniqueness

* Pass around dPath object, not just path, to maintain uniqueness

* Improve deterministic modal UI

* Fix custom dpaths
This commit is contained in:
William O'Beirne 2018-04-30 14:39:25 -04:00 committed by Daniel Ternyak
parent 82e0530bec
commit a57d17a3e0
6 changed files with 62 additions and 58 deletions

View File

@ -46,7 +46,7 @@
} }
&-addresses { &-addresses {
overflow-y: scroll; overflow-y: auto;
&-table { &-table {
width: 732px; width: 732px;
text-align: center; text-align: center;
@ -74,13 +74,19 @@
background-image: url('~assets/images/icon-external-link.svg'); background-image: url('~assets/images/icon-external-link.svg');
} }
&-na {
font-size: $font-size-xs;
opacity: 0.3;
}
// Specific selectors to override bootstrap
tbody { tbody {
tr { tr {
cursor: pointer; cursor: pointer;
}
td { td {
vertical-align: middle; vertical-align: middle;
}
} }
} }
} }

View File

@ -24,7 +24,7 @@ const WALLETS_PER_PAGE = 5;
interface Props { interface Props {
// Passed props // Passed props
isOpen?: boolean; isOpen?: boolean;
dPath: string; dPath: DPath;
dPaths: DPath[]; dPaths: DPath[];
publicKey?: string; publicKey?: string;
chainCode?: string; chainCode?: string;
@ -42,11 +42,11 @@ interface Props {
onCancel(): void; onCancel(): void;
onConfirmAddress(address: string, addressIndex: number): void; onConfirmAddress(address: string, addressIndex: number): void;
onPathChange(path: string): void; onPathChange(dPath: DPath): void;
} }
interface State { interface State {
currentLabel: string; currentDPath: DPath;
selectedAddress: string; selectedAddress: string;
selectedAddrIndex: number; selectedAddrIndex: number;
isCustomPath: boolean; isCustomPath: boolean;
@ -65,7 +65,7 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
selectedAddrIndex: 0, selectedAddrIndex: 0,
isCustomPath: false, isCustomPath: false,
customPath: '', customPath: '',
currentLabel: '', currentDPath: this.props.dPath,
page: 0 page: 0
}; };
@ -86,7 +86,7 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
} }
public render() { public render() {
const { wallets, desiredToken, network, tokens, dPath, dPaths, onCancel } = this.props; const { wallets, desiredToken, network, tokens, dPaths, onCancel } = this.props;
const { selectedAddress, customPath, page } = this.state; const { selectedAddress, customPath, page } = this.state;
const buttons: IButton[] = [ const buttons: IButton[] = [
@ -119,7 +119,7 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
<div className="DWModal-path-select"> <div className="DWModal-path-select">
<Select <Select
name="fieldDPath" name="fieldDPath"
value={this.state.currentLabel || this.findDPath('value', dPath).value} value={this.state.currentDPath}
onChange={this.handleChangePath} onChange={this.handleChangePath}
options={dPaths.concat([customDPath])} options={dPaths.concat([customDPath])}
optionRenderer={this.renderDPathOption} optionRenderer={this.renderDPathOption}
@ -128,7 +128,7 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
searchable={false} searchable={false}
/> />
</div> </div>
{this.state.currentLabel === customDPath.label && ( {this.state.currentDPath.label === customDPath.label && (
<React.Fragment> <React.Fragment>
<div className="DWModal-path-custom"> <div className="DWModal-path-custom">
<Input <Input
@ -199,12 +199,12 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
const { dPath, publicKey, chainCode, seed } = props; const { dPath, publicKey, chainCode, seed } = props;
if (dPath && ((publicKey && chainCode) || seed)) { if (dPath && ((publicKey && chainCode) || seed)) {
if (isValidPath(dPath)) { if (isValidPath(dPath.value)) {
this.props.getDeterministicWallets({ this.props.getDeterministicWallets({
seed, seed,
dPath,
publicKey, publicKey,
chainCode, chainCode,
dPath: dPath.value,
limit: WALLETS_PER_PAGE, limit: WALLETS_PER_PAGE,
offset: WALLETS_PER_PAGE * this.state.page offset: WALLETS_PER_PAGE * this.state.page
}); });
@ -214,19 +214,12 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
} }
} }
private findDPath = (prop: keyof DPath, cmp: string) => {
return this.props.dPaths.find(d => d[prop] === cmp) || customDPath;
};
private handleChangePath = (newPath: DPath) => { private handleChangePath = (newPath: DPath) => {
const { value: dPathLabel } = newPath; if (newPath.value === customDPath.value) {
const { value } = this.findDPath('value', dPathLabel); this.setState({ isCustomPath: true, currentDPath: newPath });
if (value === customDPath.value) {
this.setState({ isCustomPath: true, currentLabel: dPathLabel });
} else { } else {
this.setState({ isCustomPath: false, currentLabel: dPathLabel }); this.setState({ isCustomPath: false, currentDPath: newPath });
this.props.onPathChange(value); this.props.onPathChange(newPath);
} }
}; };
@ -235,11 +228,14 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
}; };
private handleSubmitCustomPath = (ev: React.FormEvent<HTMLFormElement>) => { private handleSubmitCustomPath = (ev: React.FormEvent<HTMLFormElement>) => {
const { customPath, currentLabel } = this.state; const { customPath, currentDPath } = this.state;
ev.preventDefault(); ev.preventDefault();
if (currentLabel === customDPath.label && isValidPath(customPath)) { if (currentDPath.value === customDPath.value && isValidPath(customPath)) {
this.props.onPathChange(customPath); this.props.onPathChange({
label: customDPath.label,
value: customPath
});
} }
}; };
@ -309,16 +305,16 @@ class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
/> />
</td> </td>
<td> <td>
{token ? ( {desiredToken ? (
<UnitDisplay <UnitDisplay
decimal={token.decimal} decimal={token ? token.decimal : 0}
value={token.value} value={token ? token.value : null}
symbol={desiredToken} symbol={desiredToken}
displayShortBalance={true} displayShortBalance={true}
checkOffline={true} checkOffline={true}
/> />
) : ( ) : (
'???' <span className="DWModal-addresses-table-na">N/A</span>
)} )}
</td> </td>
<td> <td>

View File

@ -26,7 +26,7 @@ interface StateProps {
interface State { interface State {
publicKey: string; publicKey: string;
chainCode: string; chainCode: string;
dPath: string; dPath: DPath;
error: string | null; error: string | null;
isLoading: boolean; isLoading: boolean;
showTip: boolean; showTip: boolean;
@ -38,7 +38,7 @@ class LedgerNanoSDecryptClass extends PureComponent<Props, State> {
public state: State = { public state: State = {
publicKey: '', publicKey: '',
chainCode: '', chainCode: '',
dPath: this.props.dPath ? this.props.dPath.value : '', dPath: this.props.dPath || this.props.dPaths[0],
error: null, error: null,
isLoading: false, isLoading: false,
showTip: false showTip: false
@ -51,8 +51,8 @@ class LedgerNanoSDecryptClass extends PureComponent<Props, State> {
}; };
public componentWillReceiveProps(nextProps: Props) { public componentWillReceiveProps(nextProps: Props) {
if (this.props.dPath !== nextProps.dPath) { if (this.props.dPath !== nextProps.dPath && nextProps.dPath) {
this.setState({ dPath: nextProps.dPath ? nextProps.dPath.value : '' }); this.setState({ dPath: nextProps.dPath });
} }
} }
@ -123,11 +123,11 @@ class LedgerNanoSDecryptClass extends PureComponent<Props, State> {
); );
} }
private handlePathChange = (dPath: string) => { private handlePathChange = (dPath: DPath) => {
this.handleConnect(dPath); this.handleConnect(dPath);
}; };
private handleConnect = (dPath: string = this.state.dPath) => { private handleConnect = (dPath: DPath) => {
this.setState({ this.setState({
isLoading: true, isLoading: true,
error: null, error: null,
@ -136,7 +136,7 @@ class LedgerNanoSDecryptClass extends PureComponent<Props, State> {
ledger.comm_u2f.create_async().then((comm: any) => { ledger.comm_u2f.create_async().then((comm: any) => {
new ledger.eth(comm) new ledger.eth(comm)
.getAddress_async(dPath, false, true) .getAddress_async(dPath.value, false, true)
.then(res => { .then(res => {
this.setState({ this.setState({
publicKey: res.publicKey, publicKey: res.publicKey,
@ -182,19 +182,19 @@ class LedgerNanoSDecryptClass extends PureComponent<Props, State> {
}; };
private handleUnlock = (address: string, index: number) => { private handleUnlock = (address: string, index: number) => {
this.props.onUnlock(new LedgerWallet(address, this.state.dPath, index)); this.props.onUnlock(new LedgerWallet(address, this.state.dPath.value, index));
this.reset(); this.reset();
}; };
private handleNullConnect = (): void => { private handleNullConnect = (): void => {
return this.handleConnect(); return this.handleConnect(this.state.dPath);
}; };
private reset() { private reset() {
this.setState({ this.setState({
publicKey: '', publicKey: '',
chainCode: '', chainCode: '',
dPath: this.props.dPath ? this.props.dPath.value : '' dPath: this.props.dPath || this.props.dPaths[0]
}); });
} }
} }

View File

@ -27,7 +27,7 @@ interface State {
formattedPhrase: string; formattedPhrase: string;
pass: string; pass: string;
seed: string; seed: string;
dPath: string; dPath: DPath;
} }
class MnemonicDecryptClass extends PureComponent<Props, State> { class MnemonicDecryptClass extends PureComponent<Props, State> {
@ -36,12 +36,12 @@ class MnemonicDecryptClass extends PureComponent<Props, State> {
formattedPhrase: '', formattedPhrase: '',
pass: '', pass: '',
seed: '', seed: '',
dPath: this.props.dPath.value dPath: this.props.dPath
}; };
public componentWillReceiveProps(nextProps: Props) { public componentWillReceiveProps(nextProps: Props) {
if (this.props.dPath !== nextProps.dPath) { if (this.props.dPath !== nextProps.dPath) {
this.setState({ dPath: nextProps.dPath.value }); this.setState({ dPath: nextProps.dPath });
} }
} }
@ -131,7 +131,7 @@ class MnemonicDecryptClass extends PureComponent<Props, State> {
this.setState({ seed: '' }); this.setState({ seed: '' });
}; };
private handlePathChange = (dPath: string) => { private handlePathChange = (dPath: DPath) => {
this.setState({ dPath }); this.setState({ dPath });
}; };
@ -139,7 +139,7 @@ class MnemonicDecryptClass extends PureComponent<Props, State> {
const { formattedPhrase, pass, dPath } = this.state; const { formattedPhrase, pass, dPath } = this.state;
this.props.onUnlock({ this.props.onUnlock({
path: `${dPath}/${index}`, path: `${dPath.value}/${index}`,
pass, pass,
phrase: formattedPhrase, phrase: formattedPhrase,
address address

View File

@ -25,7 +25,7 @@ interface StateProps {
interface State { interface State {
publicKey: string; publicKey: string;
chainCode: string; chainCode: string;
dPath: string; dPath: DPath;
error: string | null; error: string | null;
isLoading: boolean; isLoading: boolean;
} }
@ -36,14 +36,14 @@ class TrezorDecryptClass extends PureComponent<Props, State> {
public state: State = { public state: State = {
publicKey: '', publicKey: '',
chainCode: '', chainCode: '',
dPath: this.props.dPath ? this.props.dPath.value : '', dPath: this.props.dPath || this.props.dPaths[0],
error: null, error: null,
isLoading: false isLoading: false
}; };
public componentWillReceiveProps(nextProps: Props) { public componentWillReceiveProps(nextProps: Props) {
if (this.props.dPath !== nextProps.dPath) { if (this.props.dPath !== nextProps.dPath && nextProps.dPath) {
this.setState({ dPath: nextProps.dPath ? nextProps.dPath.value : '' }); this.setState({ dPath: nextProps.dPath });
} }
} }
@ -98,19 +98,19 @@ class TrezorDecryptClass extends PureComponent<Props, State> {
); );
} }
private handlePathChange = (dPath: string) => { private handlePathChange = (dPath: DPath) => {
this.setState({ dPath }); this.setState({ dPath });
this.handleConnect(dPath); this.handleConnect(dPath);
}; };
private handleConnect = (dPath: string = this.state.dPath): void => { private handleConnect = (dPath: DPath): void => {
this.setState({ this.setState({
isLoading: true, isLoading: true,
error: null error: null
}); });
(TrezorConnect as any).getXPubKey( (TrezorConnect as any).getXPubKey(
dPath, dPath.value,
(res: any) => { (res: any) => {
if (res.success) { if (res.success) {
this.setState({ this.setState({
@ -135,17 +135,19 @@ class TrezorDecryptClass extends PureComponent<Props, State> {
}; };
private handleUnlock = (address: string, index: number) => { private handleUnlock = (address: string, index: number) => {
this.props.onUnlock(new TrezorWallet(address, this.state.dPath, index)); this.props.onUnlock(new TrezorWallet(address, this.state.dPath.value, index));
this.reset(); this.reset();
}; };
private handleNullConnect = (): void => this.handleConnect(); private handleNullConnect = (): void => {
this.handleConnect(this.state.dPath);
};
private reset() { private reset() {
this.setState({ this.setState({
publicKey: '', publicKey: '',
chainCode: '', chainCode: '',
dPath: this.props.dPath ? this.props.dPath.value : '' dPath: this.props.dPath || this.props.dPaths[0]
}); });
} }
} }

View File

@ -1,6 +1,6 @@
import { InsecureWalletName, SecureWalletName, WalletName, walletNames } from 'config'; import { InsecureWalletName, SecureWalletName, WalletName, walletNames } from 'config';
import { EXTRA_PATHS } from 'config/dpaths'; import { EXTRA_PATHS } from 'config/dpaths';
import sortedUniq from 'lodash/sortedUniq'; import uniqBy from 'lodash/uniqBy';
import difference from 'lodash/difference'; import difference from 'lodash/difference';
import { StaticNetworkConfig, DPathFormats } from 'types/network'; import { StaticNetworkConfig, DPathFormats } from 'types/network';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
@ -22,7 +22,7 @@ export function getPaths(state: AppState, pathType: PathType): DPath[] {
return networkPaths; return networkPaths;
}, []) }, [])
.concat(EXTRA_PATHS); .concat(EXTRA_PATHS);
return sortedUniq(paths); return uniqBy(paths, p => `${p.label}${p.value}`);
} }
export function getSingleDPath(state: AppState, format: DPathFormat): DPath | undefined { export function getSingleDPath(state: AppState, format: DPathFormat): DPath | undefined {