Read-Only Address Wallet (#386)
* Check in. * Add read only wallet and new types for that. Convert some components to require full wallet. * Fix readonly property, fix uncaught throw. * Disable address only on some tabs. * Use FullWalletOnly render callback to handle signing. * Work around uncertain wallet type. * Fix function args. * Undo bug fix that should be done in another branch. * Disable button while address is bad. * Remove log. * Convert anonymous functions to class functions.
This commit is contained in:
parent
2030b60550
commit
5d3e461301
|
@ -35,7 +35,7 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
network: NETWORK_KEYS[0],
|
||||
hasAuth: false,
|
||||
username: '',
|
||||
password: '',
|
||||
password: ''
|
||||
};
|
||||
|
||||
public render() {
|
||||
|
@ -43,15 +43,18 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
const isHttps = window.location.protocol.includes('https');
|
||||
const invalids = this.getInvalids();
|
||||
|
||||
const buttons: IButton[] = [{
|
||||
const buttons: IButton[] = [
|
||||
{
|
||||
type: 'primary',
|
||||
text: translate('NODE_CTA'),
|
||||
onClick: this.saveAndAdd,
|
||||
disabled: !!Object.keys(invalids).length,
|
||||
}, {
|
||||
disabled: !!Object.keys(invalids).length
|
||||
},
|
||||
{
|
||||
text: translate('x_Cancel'),
|
||||
onClick: handleClose
|
||||
}];
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
@ -61,20 +64,23 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
handleClose={handleClose}
|
||||
>
|
||||
<div>
|
||||
{isHttps &&
|
||||
{isHttps && (
|
||||
<div className="alert alert-danger small">
|
||||
{translate('NODE_Warning')}
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
|
||||
<form>
|
||||
<div className="row">
|
||||
<div className="col-sm-7">
|
||||
<label>{translate('NODE_Name')}</label>
|
||||
{this.renderInput({
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'name',
|
||||
placeholder: 'My Node',
|
||||
}, invalids)}
|
||||
placeholder: 'My Node'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
<div className="col-sm-5">
|
||||
<label>Network</label>
|
||||
|
@ -84,9 +90,11 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
value={this.state.network}
|
||||
onChange={this.handleChange}
|
||||
>
|
||||
{NETWORK_KEYS.map((net) =>
|
||||
<option key={net} value={net}>{net}</option>
|
||||
)}
|
||||
{NETWORK_KEYS.map(net => (
|
||||
<option key={net} value={net}>
|
||||
{net}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -94,19 +102,25 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
<div className="row">
|
||||
<div className="col-sm-9">
|
||||
<label>URL</label>
|
||||
{this.renderInput({
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'url',
|
||||
placeholder: 'http://127.0.0.1/',
|
||||
}, invalids)}
|
||||
placeholder: 'http://127.0.0.1/'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-sm-3">
|
||||
<label>{translate('NODE_Port')}</label>
|
||||
{this.renderInput({
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'port',
|
||||
placeholder: '8545',
|
||||
type: 'number',
|
||||
}, invalids)}
|
||||
type: 'number'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
|
@ -117,13 +131,12 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
name="hasAuth"
|
||||
checked={this.state.hasAuth}
|
||||
onChange={this.handleCheckbox}
|
||||
/>
|
||||
{' '}
|
||||
/>{' '}
|
||||
<span>HTTP Basic Authentication</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{this.state.hasAuth &&
|
||||
{this.state.hasAuth && (
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<label>Username</label>
|
||||
|
@ -131,13 +144,16 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
</div>
|
||||
<div className="col-sm-6">
|
||||
<label>Password</label>
|
||||
{this.renderInput({
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'password',
|
||||
type: 'password',
|
||||
}, invalids)}
|
||||
type: 'password'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -145,30 +161,26 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
private renderInput(input: Input, invalids: { [key: string]: boolean }) {
|
||||
return <input
|
||||
return (
|
||||
<input
|
||||
className={classnames({
|
||||
'form-control': true,
|
||||
'is-invalid': this.state[input.name] && invalids[input.name],
|
||||
'is-invalid': this.state[input.name] && invalids[input.name]
|
||||
})}
|
||||
value={this.state[name]}
|
||||
onChange={this.handleChange}
|
||||
{...input}
|
||||
/>;
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private getInvalids(): { [key: string]: boolean } {
|
||||
const {
|
||||
url,
|
||||
port,
|
||||
hasAuth,
|
||||
username,
|
||||
password,
|
||||
} = this.state;
|
||||
const required = ["name", "url", "port", "network"];
|
||||
const { url, port, hasAuth, username, password } = this.state;
|
||||
const required = ['name', 'url', 'port', 'network'];
|
||||
const invalids: { [key: string]: boolean } = {};
|
||||
|
||||
// Required fields
|
||||
required.forEach((field) => {
|
||||
required.forEach(field => {
|
||||
if (!this.state[field]) {
|
||||
invalids[field] = true;
|
||||
}
|
||||
|
@ -198,9 +210,9 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
return invalids;
|
||||
}
|
||||
|
||||
private handleChange = (ev: React.FormEvent<
|
||||
HTMLInputElement | HTMLSelectElement
|
||||
>) => {
|
||||
private handleChange = (
|
||||
ev: React.FormEvent<HTMLInputElement | HTMLSelectElement>
|
||||
) => {
|
||||
const { name, value } = ev.currentTarget;
|
||||
this.setState({ [name as any]: value });
|
||||
};
|
||||
|
@ -215,13 +227,13 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
name: this.state.name.trim(),
|
||||
url: this.state.url.trim(),
|
||||
port: parseInt(this.state.port, 10),
|
||||
network: this.state.network,
|
||||
network: this.state.network
|
||||
};
|
||||
|
||||
if (this.state.hasAuth) {
|
||||
node.auth = {
|
||||
username: this.state.username,
|
||||
password: this.state.password,
|
||||
password: this.state.password
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,29 +1,60 @@
|
|||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import { donationAddressMap } from 'config/data';
|
||||
import { isValidETHAddress } from 'libs/validators';
|
||||
import { AddressOnlyWallet } from 'libs/wallet';
|
||||
|
||||
interface Props {
|
||||
onUnlock(param: any): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
address: string;
|
||||
}
|
||||
|
||||
export default class ViewOnlyDecrypt extends Component<Props, State> {
|
||||
public state = {
|
||||
address: ''
|
||||
};
|
||||
|
||||
export default class ViewOnlyDecrypt extends Component {
|
||||
public render() {
|
||||
const { address } = this.state;
|
||||
const isValid = isValidETHAddress(address);
|
||||
|
||||
return (
|
||||
<section className="col-md-4 col-sm-6">
|
||||
<div id="selectedUploadKey">
|
||||
<h4>
|
||||
{translate('ADD_Radio_2_alt')}
|
||||
</h4>
|
||||
<h4>{translate('MYWAL_Address')}</h4>
|
||||
|
||||
<div className="form-group">
|
||||
<input type="file" id="fselector" />
|
||||
<form className="form-group" onSubmit={this.openWallet}>
|
||||
<input
|
||||
className={`form-control
|
||||
${isValid ? 'is-valid' : 'is-invalid'}
|
||||
`}
|
||||
onChange={this.changeAddress}
|
||||
value={address}
|
||||
placeholder={donationAddressMap.ETH}
|
||||
/>
|
||||
|
||||
<a
|
||||
className="btn-file marg-v-sm"
|
||||
id="aria1"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
{translate('ADD_Radio_2_short')}
|
||||
</a>
|
||||
</div>
|
||||
<button className="btn btn-primary btn-block" disabled={!isValid}>
|
||||
{translate('NAV_ViewWallet')}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
private changeAddress = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
this.setState({ address: ev.currentTarget.value });
|
||||
};
|
||||
|
||||
private openWallet = (ev: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
const { address } = this.state;
|
||||
ev.preventDefault();
|
||||
if (isValidETHAddress(address)) {
|
||||
const wallet = new AddressOnlyWallet(address);
|
||||
this.props.onUnlock(wallet);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -30,7 +30,6 @@ const WALLETS = {
|
|||
component: Web3Decrypt,
|
||||
initialParams: {},
|
||||
unlock: unlockWeb3,
|
||||
disabled: false,
|
||||
helpLink:
|
||||
'https://myetherwallet.github.io/knowledge-base/migration/moving-from-private-key-to-metamask.html'
|
||||
},
|
||||
|
@ -39,7 +38,6 @@ const WALLETS = {
|
|||
component: LedgerNanoSDecrypt,
|
||||
initialParams: {},
|
||||
unlock: setWallet,
|
||||
disabled: false,
|
||||
helpLink:
|
||||
'https://ledger.zendesk.com/hc/en-us/articles/115005200009-How-to-use-MyEtherWallet-with-Ledger'
|
||||
},
|
||||
|
@ -48,7 +46,6 @@ const WALLETS = {
|
|||
component: TrezorDecrypt,
|
||||
initialParams: {},
|
||||
unlock: setWallet,
|
||||
disabled: false,
|
||||
helpLink: 'https://doc.satoshilabs.com/trezor-apps/mew.html'
|
||||
},
|
||||
'keystore-file': {
|
||||
|
@ -59,7 +56,6 @@ const WALLETS = {
|
|||
password: ''
|
||||
},
|
||||
unlock: unlockKeystore,
|
||||
disabled: false,
|
||||
helpLink:
|
||||
'https://myetherwallet.github.io/knowledge-base/private-keys-passwords/difference-beween-private-key-and-keystore-file.html'
|
||||
},
|
||||
|
@ -68,7 +64,6 @@ const WALLETS = {
|
|||
component: MnemonicDecrypt,
|
||||
initialParams: {},
|
||||
unlock: unlockMnemonic,
|
||||
disabled: false,
|
||||
helpLink:
|
||||
'https://myetherwallet.github.io/knowledge-base/private-keys-passwords/difference-beween-private-key-and-keystore-file.html'
|
||||
},
|
||||
|
@ -80,14 +75,14 @@ const WALLETS = {
|
|||
password: ''
|
||||
},
|
||||
unlock: unlockPrivateKey,
|
||||
disabled: false,
|
||||
helpLink:
|
||||
'https://myetherwallet.github.io/knowledge-base/private-keys-passwords/difference-beween-private-key-and-keystore-file.html'
|
||||
},
|
||||
'view-only': {
|
||||
lid: 'View with Address Only',
|
||||
component: ViewOnlyDecrypt,
|
||||
disabled: true,
|
||||
initialParams: {},
|
||||
unlock: setWallet,
|
||||
helpLink: ''
|
||||
}
|
||||
};
|
||||
|
@ -100,6 +95,7 @@ interface Props {
|
|||
UnlockKeystoreAction | UnlockMnemonicAction | UnlockPrivateKeyAction
|
||||
>;
|
||||
offline: boolean;
|
||||
allowReadOnly?: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -139,8 +135,12 @@ export class WalletDecrypt extends Component<Props, State> {
|
|||
|
||||
public buildWalletOptions() {
|
||||
return map(WALLETS, (wallet, key) => {
|
||||
const isSelected = this.state.selectedWalletKey === key;
|
||||
const { helpLink } = wallet;
|
||||
const isSelected = this.state.selectedWalletKey === key;
|
||||
const isDisabled =
|
||||
this.isOnlineRequiredWalletAndOffline(key) ||
|
||||
(!this.props.allowReadOnly && wallet.component === ViewOnlyDecrypt);
|
||||
|
||||
return (
|
||||
<label className="radio" key={key}>
|
||||
<input
|
||||
|
@ -150,10 +150,8 @@ export class WalletDecrypt extends Component<Props, State> {
|
|||
name="decryption-choice-radio-group"
|
||||
value={key}
|
||||
checked={isSelected}
|
||||
disabled={isDisabled}
|
||||
onChange={this.handleDecryptionChoiceChange}
|
||||
disabled={
|
||||
wallet.disabled || this.isOnlineRequiredWalletAndOffline(key)
|
||||
}
|
||||
/>
|
||||
<span id={`${key}-label`}>{translate(wallet.lid)}</span>
|
||||
{helpLink ? <Help link={helpLink} /> : null}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from 'reducers';
|
||||
import { IWallet, IFullWallet } from 'libs/wallet';
|
||||
|
||||
interface Props {
|
||||
wallet: IWallet;
|
||||
withFullWallet(wallet: IFullWallet): React.ReactElement<any>;
|
||||
withoutFullWallet(): React.ReactElement<any>;
|
||||
}
|
||||
|
||||
class FullWalletOnly extends React.Component<Props, {}> {
|
||||
public render() {
|
||||
const { wallet, withFullWallet, withoutFullWallet } = this.props;
|
||||
if (!wallet || wallet.isReadOnly) {
|
||||
if (withoutFullWallet) {
|
||||
return withoutFullWallet();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return withFullWallet(wallet);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect((state: AppState) => ({
|
||||
wallet: state.wallet.inst
|
||||
}))(FullWalletOnly);
|
|
@ -7,6 +7,7 @@ import { AppState } from 'reducers';
|
|||
interface Props {
|
||||
title: React.ReactElement<any>;
|
||||
wallet: IWallet;
|
||||
allowReadOnly?: boolean;
|
||||
}
|
||||
interface State {
|
||||
expanded: boolean;
|
||||
|
@ -28,7 +29,7 @@ export class UnlockHeader extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const { title } = this.props;
|
||||
const { title, allowReadOnly } = this.props;
|
||||
return (
|
||||
<article className="collapse-container">
|
||||
<div>
|
||||
|
@ -37,7 +38,7 @@ export class UnlockHeader extends React.Component<Props, State> {
|
|||
</a>
|
||||
<h1>{title}</h1>
|
||||
</div>
|
||||
{this.state.expanded && <WalletDecrypt />}
|
||||
{this.state.expanded && <WalletDecrypt allowReadOnly={allowReadOnly} />}
|
||||
{this.state.expanded && <hr />}
|
||||
</article>
|
||||
);
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
TChangeLanguage,
|
||||
TChangeNodeIntent,
|
||||
TAddCustomNode,
|
||||
TRemoveCustomNode,
|
||||
TRemoveCustomNode
|
||||
} from 'actions/config';
|
||||
import { AlphaAgreement, Footer, Header } from 'components';
|
||||
import React, { Component } from 'react';
|
||||
|
@ -52,7 +52,7 @@ class TabSection extends Component<Props, {}> {
|
|||
changeNodeIntent,
|
||||
changeGasPrice,
|
||||
addCustomNode,
|
||||
removeCustomNode,
|
||||
removeCustomNode
|
||||
} = this.props;
|
||||
|
||||
const headerProps = {
|
||||
|
@ -67,7 +67,7 @@ class TabSection extends Component<Props, {}> {
|
|||
changeNodeIntent,
|
||||
changeGasPrice,
|
||||
addCustomNode,
|
||||
removeCustomNode,
|
||||
removeCustomNode
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -92,7 +92,7 @@ function mapStateToProps(state: AppState) {
|
|||
languageSelection: state.config.languageSelection,
|
||||
gasPriceGwei: state.config.gasPriceGwei,
|
||||
customNodes: state.config.customNodes,
|
||||
latestBlock: state.config.latestBlock,
|
||||
latestBlock: state.config.latestBlock
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -101,5 +101,5 @@ export default connect(mapStateToProps, {
|
|||
changeLanguage: dChangeLanguage,
|
||||
changeNodeIntent: dChangeNodeIntent,
|
||||
addCustomNode: dAddCustomNode,
|
||||
removeCustomNode: dRemoveCustomNode,
|
||||
removeCustomNode: dRemoveCustomNode
|
||||
})(TabSection);
|
||||
|
|
|
@ -127,6 +127,10 @@ export const deployHOC = PassedComponent => {
|
|||
value
|
||||
};
|
||||
|
||||
if (!props.wallet || props.wallet.isReadOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
return makeAndSignTx(
|
||||
props.wallet,
|
||||
props.nodeLib,
|
||||
|
@ -139,6 +143,10 @@ export const deployHOC = PassedComponent => {
|
|||
};
|
||||
|
||||
private getAddressAndNonce = async () => {
|
||||
if (!this.props.wallet || this.props.wallet.isReadOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const address = await this.props.wallet.getAddressString();
|
||||
const nonce = await this.props.nodeLib
|
||||
.getTransactionCount(address)
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { Wei } from 'libs/units';
|
||||
import { IWallet, Balance } from 'libs/wallet';
|
||||
import { Balance } from 'libs/wallet';
|
||||
import { RPCNode } from 'libs/nodes';
|
||||
import { NodeConfig, NetworkConfig } from 'config/data';
|
||||
import { TBroadcastTx } from 'actions/wallet';
|
||||
import { TShowNotification } from 'actions/notifications';
|
||||
import { AppState } from 'reducers';
|
||||
|
||||
export interface Props {
|
||||
wallet: IWallet;
|
||||
wallet: AppState['wallet']['inst'];
|
||||
balance: Balance;
|
||||
node: NodeConfig;
|
||||
nodeLib: RPCNode;
|
||||
|
|
|
@ -4,12 +4,12 @@ import { toWei, Wei, getDecimal } from 'libs/units';
|
|||
import { connect } from 'react-redux';
|
||||
import { showNotification, TShowNotification } from 'actions/notifications';
|
||||
import { broadcastTx, TBroadcastTx } from 'actions/wallet';
|
||||
import { IWallet, Balance } from 'libs/wallet';
|
||||
import { IFullWallet, Balance } from 'libs/wallet';
|
||||
import { RPCNode } from 'libs/nodes';
|
||||
import { NodeConfig, NetworkConfig } from 'config/data';
|
||||
|
||||
export interface IWithTx {
|
||||
wallet: IWallet;
|
||||
wallet: IFullWallet;
|
||||
balance: Balance;
|
||||
node: NodeConfig;
|
||||
nodeLib: RPCNode;
|
||||
|
|
|
@ -285,6 +285,7 @@ export class SendTransaction extends React.Component<Props, State> {
|
|||
) : null}
|
||||
</div>
|
||||
}
|
||||
allowReadOnly={true}
|
||||
/>
|
||||
<NavigationPrompt
|
||||
when={unlocked}
|
||||
|
@ -364,9 +365,7 @@ export class SendTransaction extends React.Component<Props, State> {
|
|||
{isLedgerWallet || isTrezorWallet ? (
|
||||
<div>
|
||||
<p>
|
||||
<b>
|
||||
Confirm transaction on hardware wallet
|
||||
</b>
|
||||
<b>Confirm transaction on hardware wallet</b>
|
||||
</p>
|
||||
<Spinner size="x2" />
|
||||
</div>
|
||||
|
@ -512,6 +511,10 @@ export class SendTransaction extends React.Component<Props, State> {
|
|||
|
||||
public async getFormattedTxFromState(): Promise<TransactionWithoutGas> {
|
||||
const { wallet } = this.props;
|
||||
if (wallet.isReadOnly) {
|
||||
throw new Error('Wallet is read-only');
|
||||
}
|
||||
|
||||
const { token, unit, value, to, data } = this.state;
|
||||
const transactionInput: TransactionInput = {
|
||||
token,
|
||||
|
@ -694,6 +697,7 @@ export class SendTransaction extends React.Component<Props, State> {
|
|||
await this.resetJustTx();
|
||||
const { nodeLib, wallet, gasPrice, network, offline } = this.props;
|
||||
const { token, unit, value, to, data, gasLimit, nonce } = this.state;
|
||||
|
||||
const chainId = network.chainId;
|
||||
const transactionInput = {
|
||||
token,
|
||||
|
@ -704,6 +708,10 @@ export class SendTransaction extends React.Component<Props, State> {
|
|||
};
|
||||
const bigGasLimit = Wei(gasLimit);
|
||||
try {
|
||||
if (wallet.isReadOnly) {
|
||||
throw new Error('Wallet is read-only');
|
||||
}
|
||||
|
||||
const signedTx = await generateCompleteTransaction(
|
||||
wallet,
|
||||
nodeLib,
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
import translate from 'translations';
|
||||
import { ISignedMessage } from 'libs/signing';
|
||||
import { IFullWallet } from 'libs/wallet';
|
||||
import { TShowNotification } from 'actions/notifications';
|
||||
|
||||
interface Props {
|
||||
wallet: IFullWallet;
|
||||
message: string;
|
||||
showNotification: TShowNotification;
|
||||
onSignMessage(msg: ISignedMessage): any;
|
||||
}
|
||||
|
||||
export default class SignMessageButton extends React.Component<Props, {}> {
|
||||
public render() {
|
||||
return (
|
||||
<button
|
||||
className="SignMessage-sign btn btn-primary btn-lg"
|
||||
onClick={this.handleSignMessage}
|
||||
>
|
||||
{translate('NAV_SignMsg')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
private handleSignMessage = async () => {
|
||||
const { wallet, message, showNotification, onSignMessage } = this.props;
|
||||
|
||||
try {
|
||||
const signedMessage: ISignedMessage = {
|
||||
address: await wallet.getAddressString(),
|
||||
message,
|
||||
signature: await wallet.signMessage(message),
|
||||
version: '2'
|
||||
};
|
||||
|
||||
onSignMessage(signedMessage);
|
||||
showNotification(
|
||||
'success',
|
||||
`Successfully signed message with address ${signedMessage.address}.`
|
||||
);
|
||||
} catch (err) {
|
||||
showNotification('danger', `Error signing message: ${err.message}`);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,28 +1,26 @@
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import classnames from 'classnames';
|
||||
import { IWallet } from 'libs/wallet/IWallet';
|
||||
import WalletDecrypt from 'components/WalletDecrypt';
|
||||
import translate from 'translations';
|
||||
import { showNotification, TShowNotification } from 'actions/notifications';
|
||||
import { ISignedMessage } from 'libs/signing';
|
||||
import { AppState } from 'reducers';
|
||||
import { IFullWallet } from 'libs/wallet';
|
||||
import FullWalletOnly from 'components/renderCbs/FullWalletOnly';
|
||||
import SignButton from './SignButton';
|
||||
import './index.scss';
|
||||
|
||||
interface Props {
|
||||
wallet: IWallet;
|
||||
showNotification: TShowNotification;
|
||||
}
|
||||
|
||||
interface State {
|
||||
message: string;
|
||||
signMessageError: string;
|
||||
signedMessage: ISignedMessage | null;
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
message: '',
|
||||
signMessageError: '',
|
||||
signedMessage: null
|
||||
};
|
||||
|
||||
|
@ -33,7 +31,6 @@ export class SignMessage extends Component<Props, State> {
|
|||
public state: State = initialState;
|
||||
|
||||
public render() {
|
||||
const { wallet } = this.props;
|
||||
const { message, signedMessage } = this.state;
|
||||
|
||||
const messageBoxClass = classnames([
|
||||
|
@ -56,14 +53,10 @@ export class SignMessage extends Component<Props, State> {
|
|||
<div className="SignMessage-help">{translate('MSG_info2')}</div>
|
||||
</div>
|
||||
|
||||
{!!wallet && (
|
||||
<button
|
||||
className="SignMessage-sign btn btn-primary btn-lg"
|
||||
onClick={this.handleSignMessage}
|
||||
>
|
||||
{translate('NAV_SignMsg')}
|
||||
</button>
|
||||
)}
|
||||
<FullWalletOnly
|
||||
withFullWallet={this.renderSignButton}
|
||||
withoutFullWallet={this.renderUnlock}
|
||||
/>
|
||||
|
||||
{!!signedMessage && (
|
||||
<div>
|
||||
|
@ -79,53 +72,33 @@ export class SignMessage extends Component<Props, State> {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!wallet && <WalletDecrypt />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private handleSignMessage = async () => {
|
||||
const { wallet } = this.props;
|
||||
const { message } = this.state;
|
||||
|
||||
if (!wallet) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const signedMessage: ISignedMessage = {
|
||||
address: await wallet.getAddressString(),
|
||||
message,
|
||||
signature: await wallet.signMessage(message),
|
||||
version: '2'
|
||||
};
|
||||
|
||||
this.setState({ signedMessage });
|
||||
this.props.showNotification(
|
||||
'success',
|
||||
`Successfully signed message with address ${signedMessage.address}.`
|
||||
);
|
||||
} catch (err) {
|
||||
this.props.showNotification(
|
||||
'danger',
|
||||
`Error signing message: ${err.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private handleMessageChange = (e: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
const message = e.currentTarget.value;
|
||||
this.setState({ message });
|
||||
};
|
||||
}
|
||||
|
||||
function mapStateToProps(state: AppState) {
|
||||
return {
|
||||
wallet: state.wallet.inst
|
||||
private onSignMessage = (signedMessage: ISignedMessage) => {
|
||||
this.setState({ signedMessage });
|
||||
};
|
||||
|
||||
private renderSignButton(fullWallet: IFullWallet) {
|
||||
return (
|
||||
<SignButton
|
||||
wallet={fullWallet}
|
||||
message={this.state.message}
|
||||
showNotification={this.props.showNotification}
|
||||
onSignMessage={this.onSignMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private renderUnlock() {
|
||||
return <WalletDecrypt />;
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
showNotification
|
||||
})(SignMessage);
|
||||
export default connect(null, { showNotification })(SignMessage);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import AbiFunction, { IUserSendParams, ISendParams } from './ABIFunction';
|
||||
import { IWallet } from 'libs/wallet/IWallet';
|
||||
import { IFullWallet } from 'libs/wallet/IWallet';
|
||||
import { RPCNode } from 'libs/nodes';
|
||||
import { ContractOutputMappings } from './types';
|
||||
import { Wei } from 'libs/units';
|
||||
|
@ -12,7 +12,7 @@ const ABIFUNC_METHOD_NAMES = [
|
|||
];
|
||||
|
||||
export interface ISetConfigForTx {
|
||||
wallet: IWallet;
|
||||
wallet: IFullWallet;
|
||||
nodeLib: RPCNode;
|
||||
chainId: number;
|
||||
gasPrice: Wei;
|
||||
|
@ -36,9 +36,8 @@ export default class Contract {
|
|||
.setGasPrice(gasPrice);
|
||||
|
||||
public static getFunctions = (contract: Contract) =>
|
||||
Object.getOwnPropertyNames(
|
||||
contract
|
||||
).reduce((accu, currContractMethodName) => {
|
||||
Object.getOwnPropertyNames(contract).reduce(
|
||||
(accu, currContractMethodName) => {
|
||||
const currContractMethod = contract[currContractMethodName];
|
||||
const methodNames = Object.getOwnPropertyNames(currContractMethod);
|
||||
|
||||
|
@ -50,11 +49,13 @@ export default class Contract {
|
|||
return isFunc
|
||||
? { ...accu, [currContractMethodName]: currContractMethod }
|
||||
: accu;
|
||||
}, {});
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
public address: string;
|
||||
public abi;
|
||||
private wallet: IWallet;
|
||||
private wallet: IFullWallet;
|
||||
private gasPrice: Wei;
|
||||
private chainId: number;
|
||||
private node: RPCNode;
|
||||
|
@ -68,7 +69,7 @@ export default class Contract {
|
|||
return this;
|
||||
};
|
||||
|
||||
public setWallet = (w: IWallet) => {
|
||||
public setWallet = (w: IFullWallet) => {
|
||||
this.wallet = w;
|
||||
return this;
|
||||
};
|
||||
|
|
|
@ -71,7 +71,7 @@ export default class EtherscanRequests extends RPCRequests {
|
|||
public getCurrentBlock(): GetCurrentBlockRequest {
|
||||
return {
|
||||
module: 'proxy',
|
||||
action: 'eth_blockNumber',
|
||||
action: 'eth_blockNumber'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ export default class RPCClient {
|
|||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...this.headers,
|
||||
...this.headers
|
||||
},
|
||||
body: JSON.stringify(this.decorateRequest(request))
|
||||
}).then(r => r.json());
|
||||
|
@ -35,7 +35,7 @@ export default class RPCClient {
|
|||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...this.headers,
|
||||
...this.headers
|
||||
},
|
||||
body: JSON.stringify(requests.map(this.decorateRequest))
|
||||
}).then(r => r.json());
|
||||
|
|
|
@ -70,7 +70,7 @@ export interface GetTransactionCountRequest extends RPCRequestBase {
|
|||
}
|
||||
|
||||
export interface GetCurrentBlockRequest extends RPCRequestBase {
|
||||
method: 'eth_blockNumber'
|
||||
method: 'eth_blockNumber';
|
||||
}
|
||||
|
||||
export type RPCRequest =
|
||||
|
|
|
@ -8,7 +8,7 @@ import { INode } from 'libs/nodes/INode';
|
|||
import { UnitKey, Wei, TokenValue, toTokenBase } from 'libs/units';
|
||||
import { isValidETHAddress } from 'libs/validators';
|
||||
import { stripHexPrefixAndLower, sanitizeHex, toHexWei } from 'libs/values';
|
||||
import { IWallet, Web3Wallet } from 'libs/wallet';
|
||||
import { IFullWallet, Web3Wallet } from 'libs/wallet';
|
||||
import { translateRaw } from 'translations';
|
||||
|
||||
export interface TransactionInput {
|
||||
|
@ -168,7 +168,7 @@ function generateTxValidation(
|
|||
export async function generateCompleteTransactionFromRawTransaction(
|
||||
node: INode,
|
||||
tx: ExtendedRawTransaction,
|
||||
wallet: IWallet,
|
||||
wallet: IFullWallet,
|
||||
token: Token | null | undefined,
|
||||
skipValidation: boolean,
|
||||
offline?: boolean
|
||||
|
@ -213,7 +213,7 @@ export async function generateCompleteTransactionFromRawTransaction(
|
|||
}
|
||||
|
||||
export async function formatTxInput(
|
||||
wallet: IWallet,
|
||||
wallet: IFullWallet,
|
||||
{ token, unit, value, to, data }: TransactionInput
|
||||
): Promise<TransactionWithoutGas> {
|
||||
if (unit === 'ether') {
|
||||
|
@ -265,7 +265,7 @@ export async function confirmAndSendWeb3Transaction(
|
|||
}
|
||||
|
||||
export async function generateCompleteTransaction(
|
||||
wallet: IWallet,
|
||||
wallet: IFullWallet,
|
||||
nodeLib: RPCNode,
|
||||
gasPrice: Wei,
|
||||
gasLimit: Wei,
|
||||
|
|
|
@ -1,7 +1,18 @@
|
|||
import { RawTransaction } from 'libs/transaction';
|
||||
|
||||
export interface IWallet {
|
||||
interface IBaseWallet {
|
||||
isReadOnly?: boolean;
|
||||
getAddressString(): Promise<string> | string;
|
||||
}
|
||||
|
||||
export interface IReadOnlyWallet extends IBaseWallet {
|
||||
isReadOnly: true;
|
||||
}
|
||||
|
||||
export interface IFullWallet extends IBaseWallet {
|
||||
isReadOnly?: false;
|
||||
signRawTransaction(tx: RawTransaction): Promise<string> | string;
|
||||
signMessage(msg: string): Promise<string> | string;
|
||||
}
|
||||
|
||||
export type IWallet = IReadOnlyWallet | IFullWallet;
|
||||
|
|
|
@ -3,10 +3,10 @@ import LedgerEth from 'vendor/ledger-eth';
|
|||
import EthTx from 'ethereumjs-tx';
|
||||
import { addHexPrefix, rlp } from 'ethereumjs-util';
|
||||
import { DeterministicWallet } from './deterministic';
|
||||
import { IWallet } from '../IWallet';
|
||||
import { IFullWallet } from '../IWallet';
|
||||
import { RawTransaction } from 'libs/transaction';
|
||||
|
||||
export class LedgerWallet extends DeterministicWallet implements IWallet {
|
||||
export class LedgerWallet extends DeterministicWallet implements IFullWallet {
|
||||
private ledger: any;
|
||||
private ethApp: any;
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@ import { RawTransaction } from 'libs/transaction';
|
|||
import { stripHexPrefixAndLower } from 'libs/values';
|
||||
import TrezorConnect from 'vendor/trezor-connect';
|
||||
import { DeterministicWallet } from './deterministic';
|
||||
import { IWallet } from '../IWallet';
|
||||
import { IFullWallet } from '../IWallet';
|
||||
|
||||
export class TrezorWallet extends DeterministicWallet implements IWallet {
|
||||
export class TrezorWallet extends DeterministicWallet implements IFullWallet {
|
||||
public signRawTransaction(tx: RawTransaction): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
(TrezorConnect as any).ethereumSignTx(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export { IWallet } from './IWallet';
|
||||
export { IWallet, IReadOnlyWallet, IFullWallet } from './IWallet';
|
||||
export { Balance } from './balance';
|
||||
export * from './deterministic';
|
||||
export * from './non-deterministic';
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import { IReadOnlyWallet } from '../IWallet';
|
||||
|
||||
export default class AddressOnlyWallet implements IReadOnlyWallet {
|
||||
public address = '';
|
||||
public readonly isReadOnly = true;
|
||||
|
||||
constructor(address: string) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public getAddressString() {
|
||||
return this.address;
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ interface ISignWrapper {
|
|||
unlock();
|
||||
}
|
||||
|
||||
type WrappedWallet = IFullWallet & ISignWrapper;
|
||||
export type WrappedWallet = IFullWallet & ISignWrapper;
|
||||
|
||||
export const signWrapper = (walletToWrap: IFullWallet): WrappedWallet =>
|
||||
Object.assign(walletToWrap, {
|
||||
|
|
|
@ -3,6 +3,7 @@ import { fromEtherWallet } from 'ethereumjs-wallet/thirdparty';
|
|||
import { signWrapper } from './helpers';
|
||||
import { decryptPrivKey } from 'libs/decrypt';
|
||||
import Web3Wallet from './web3';
|
||||
import AddressOnlyWallet from './address';
|
||||
|
||||
const EncryptedPrivateKeyWallet = (
|
||||
encryptedPrivateKey: string,
|
||||
|
@ -26,5 +27,6 @@ export {
|
|||
MewV1Wallet,
|
||||
PrivKeyWallet,
|
||||
UtcWallet,
|
||||
Web3Wallet
|
||||
Web3Wallet,
|
||||
AddressOnlyWallet
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { IWallet } from '../IWallet';
|
||||
import { IFullWallet } from '../IWallet';
|
||||
import { ExtendedRawTransaction } from 'libs/transaction';
|
||||
import { networkIdToName } from 'libs/values';
|
||||
import { bufferToHex } from 'ethereumjs-util';
|
||||
|
||||
export default class Web3Wallet implements IWallet {
|
||||
export default class Web3Wallet implements IFullWallet {
|
||||
private web3: any;
|
||||
private address: string;
|
||||
private network: string;
|
||||
|
|
|
@ -5,14 +5,10 @@ import {
|
|||
AddCustomNodeAction,
|
||||
RemoveCustomNodeAction,
|
||||
SetLatestBlockAction,
|
||||
ConfigAction,
|
||||
ConfigAction
|
||||
} from 'actions/config';
|
||||
import { TypeKeys } from 'actions/config/constants';
|
||||
import {
|
||||
NODES,
|
||||
NodeConfig,
|
||||
CustomNodeConfig,
|
||||
} from '../config/data';
|
||||
import { NODES, NodeConfig, CustomNodeConfig } from '../config/data';
|
||||
import { makeCustomNodeId } from 'utils/node';
|
||||
|
||||
export interface State {
|
||||
|
@ -38,7 +34,7 @@ export const INITIAL_STATE: State = {
|
|||
offline: false,
|
||||
forceOffline: false,
|
||||
customNodes: [],
|
||||
latestBlock: "???",
|
||||
latestBlock: '???'
|
||||
};
|
||||
|
||||
function changeLanguage(state: State, action: ChangeLanguageAction): State {
|
||||
|
@ -53,14 +49,14 @@ function changeNode(state: State, action: ChangeNodeAction): State {
|
|||
...state,
|
||||
nodeSelection: action.payload.nodeSelection,
|
||||
node: action.payload.node,
|
||||
isChangingNode: false,
|
||||
isChangingNode: false
|
||||
};
|
||||
}
|
||||
|
||||
function changeNodeIntent(state: State): State {
|
||||
return {
|
||||
...state,
|
||||
isChangingNode: true,
|
||||
isChangingNode: true
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -88,10 +84,7 @@ function forceOffline(state: State): State {
|
|||
function addCustomNode(state: State, action: AddCustomNodeAction): State {
|
||||
return {
|
||||
...state,
|
||||
customNodes: [
|
||||
...state.customNodes,
|
||||
action.payload,
|
||||
],
|
||||
customNodes: [...state.customNodes, action.payload]
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -99,16 +92,16 @@ function removeCustomNode(state: State, action: RemoveCustomNodeAction): State {
|
|||
const id = makeCustomNodeId(action.payload);
|
||||
return {
|
||||
...state,
|
||||
customNodes: state.customNodes.filter((cn) => cn !== action.payload),
|
||||
nodeSelection: id === state.nodeSelection ?
|
||||
defaultNode : state.nodeSelection,
|
||||
customNodes: state.customNodes.filter(cn => cn !== action.payload),
|
||||
nodeSelection:
|
||||
id === state.nodeSelection ? defaultNode : state.nodeSelection
|
||||
};
|
||||
}
|
||||
|
||||
function setLatestBlock(state: State, action: SetLatestBlockAction): State {
|
||||
return {
|
||||
...state,
|
||||
latestBlock: action.payload,
|
||||
latestBlock: action.payload
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -6,13 +6,15 @@ export function makeCustomNodeId(config: CustomNodeConfig): string {
|
|||
}
|
||||
|
||||
export function getCustomNodeConfigFromId(
|
||||
id: string, configs: CustomNodeConfig[]
|
||||
id: string,
|
||||
configs: CustomNodeConfig[]
|
||||
): CustomNodeConfig | undefined {
|
||||
return configs.find((node) => makeCustomNodeId(node) === id);
|
||||
return configs.find(node => makeCustomNodeId(node) === id);
|
||||
}
|
||||
|
||||
export function getNodeConfigFromId(
|
||||
id: string, configs: CustomNodeConfig[]
|
||||
id: string,
|
||||
configs: CustomNodeConfig[]
|
||||
): NodeConfig | undefined {
|
||||
if (NODES[id]) {
|
||||
return NODES[id];
|
||||
|
@ -30,7 +32,7 @@ export function makeNodeConfigFromCustomConfig(
|
|||
return {
|
||||
network: config.network,
|
||||
lib: new CustomNode(config),
|
||||
service: "your custom node",
|
||||
estimateGas: true,
|
||||
service: 'your custom node',
|
||||
estimateGas: true
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue