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:
William O'Beirne 2017-11-29 15:14:57 -08:00 committed by Daniel Ternyak
parent 2030b60550
commit 5d3e461301
28 changed files with 400 additions and 272 deletions

View File

@ -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[] = [{
type: 'primary',
text: translate('NODE_CTA'),
onClick: this.saveAndAdd,
disabled: !!Object.keys(invalids).length,
}, {
text: translate('x_Cancel'),
onClick: handleClose
}];
const buttons: IButton[] = [
{
type: 'primary',
text: translate('NODE_CTA'),
onClick: this.saveAndAdd,
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({
name: 'name',
placeholder: 'My Node',
}, invalids)}
{this.renderInput(
{
name: 'name',
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({
name: 'url',
placeholder: 'http://127.0.0.1/',
}, invalids)}
{this.renderInput(
{
name: 'url',
placeholder: 'http://127.0.0.1/'
},
invalids
)}
</div>
<div className="col-sm-3">
<label>{translate('NODE_Port')}</label>
{this.renderInput({
name: 'port',
placeholder: '8545',
type: 'number',
}, invalids)}
{this.renderInput(
{
name: 'port',
placeholder: '8545',
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({
name: 'password',
type: 'password',
}, invalids)}
{this.renderInput(
{
name: 'password',
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
className={classnames({
'form-control': true,
'is-invalid': this.state[input.name] && invalids[input.name],
})}
value={this.state[name]}
onChange={this.handleChange}
{...input}
/>;
return (
<input
className={classnames({
'form-control': true,
'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
};
}

View File

@ -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);
}
};
}

View File

@ -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}

View File

@ -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);

View File

@ -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>
);

View File

@ -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);

View File

@ -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)

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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}`);
}
};
}

View File

@ -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);

View File

@ -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,25 +36,26 @@ export default class Contract {
.setGasPrice(gasPrice);
public static getFunctions = (contract: Contract) =>
Object.getOwnPropertyNames(
contract
).reduce((accu, currContractMethodName) => {
const currContractMethod = contract[currContractMethodName];
const methodNames = Object.getOwnPropertyNames(currContractMethod);
Object.getOwnPropertyNames(contract).reduce(
(accu, currContractMethodName) => {
const currContractMethod = contract[currContractMethodName];
const methodNames = Object.getOwnPropertyNames(currContractMethod);
const isFunc = ABIFUNC_METHOD_NAMES.reduce(
(isAbiFunc, currAbiFuncMethodName) =>
isAbiFunc && methodNames.includes(currAbiFuncMethodName),
true
);
return isFunc
? { ...accu, [currContractMethodName]: currContractMethod }
: accu;
}, {});
const isFunc = ABIFUNC_METHOD_NAMES.reduce(
(isAbiFunc, currAbiFuncMethodName) =>
isAbiFunc && methodNames.includes(currAbiFuncMethodName),
true
);
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;
};

View File

@ -71,7 +71,7 @@ export default class EtherscanRequests extends RPCRequests {
public getCurrentBlock(): GetCurrentBlockRequest {
return {
module: 'proxy',
action: 'eth_blockNumber',
action: 'eth_blockNumber'
};
}
}

View File

@ -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());

View File

@ -70,7 +70,7 @@ export interface GetTransactionCountRequest extends RPCRequestBase {
}
export interface GetCurrentBlockRequest extends RPCRequestBase {
method: 'eth_blockNumber'
method: 'eth_blockNumber';
}
export type RPCRequest =

View File

@ -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,

View File

@ -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;

View File

@ -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;

View File

@ -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(
@ -51,12 +51,12 @@ export class TrezorWallet extends DeterministicWallet implements IWallet {
public signMessage = (message: string): Promise<string> => {
return new Promise((resolve, reject) => {
(TrezorConnect as any).ethereumSignMessage(
this.getPath(),
message,
this.getPath(),
message,
response => {
if (response.success) {
resolve(addHexPrefix(response.signature))
} else{
} else{
console.error(response.error)
reject(response.error)
}

View File

@ -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';

View File

@ -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;
}
}

View File

@ -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, {

View File

@ -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
};

View File

@ -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;

View File

@ -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
};
}

View File

@ -16,34 +16,34 @@ declare module 'ethereumjs-wallet/thirdparty' {
IEtherWalletUnlocked
} from 'ethereumjs-wallet';
/**
* @description import a brain wallet used by Ether.Camp
* @param input
* @param password
*/
* @description import a brain wallet used by Ether.Camp
* @param input
* @param password
*/
function fromEtherCamp(passphrase: string): IFullWallet;
/**
* @description import a wallet generated by EtherWallet
* @param input
* @param password
*/
* @description import a wallet generated by EtherWallet
* @param input
* @param password
*/
function fromEtherWallet(
input: IEtherWalletLocked | IEtherWalletUnlocked | string,
password: string
): IFullWallet;
/**
* @description import a wallet from a KryptoKit seed
* @param entropy
* @param seed
*/
* @description import a wallet from a KryptoKit seed
* @param entropy
* @param seed
*/
function fromKryptoKit(entropy: string, seed: string): IFullWallet;
/**
* @description import a brain wallet used by Quorum Wallet
* @param passphrase
* @param userid
*/
* @description import a brain wallet used by Quorum Wallet
* @param passphrase
* @param userid
*/
function fromQuorumWallet(passphrase: string, userid: string): IFullWallet;
}
@ -68,14 +68,14 @@ declare module 'ethereumjs-wallet/hdkey' {
deriveChild(index): IHDNodePublic;
/**
* @description return a Wallet instance
* @description return a Wallet instance
*/
getWallet(): IPublicKeyOnlyWallet;
}
/**
*
*
*
*
* @interface IHDNodePrivate
*/
interface IHDNodePrivate {
@ -100,19 +100,19 @@ declare module 'ethereumjs-wallet/hdkey' {
deriveChild(index): IHDNodePrivate | IHDNodePublic;
/**
* @description return a Wallet instance
* @description return a Wallet instance
*/
getWallet(): IFullWallet;
}
/**
* @description create an instance based on a seed
*/
* @description create an instance based on a seed
*/
function fromMasterSeed(seed: Buffer): IHDNodePrivate;
/**
* @description create an instance based on a BIP32 extended private or public key
*/
* @description create an instance based on a BIP32 extended private or public key
*/
function fromExtendedKey(key: string): IHDNodePrivate | IHDNodePublic;
}
@ -227,7 +227,7 @@ declare module 'ethereumjs-wallet' {
getAddress(): Buffer;
/**
*
*
* @description return the address
*/
getAddressString(): string;
@ -262,61 +262,61 @@ declare module 'ethereumjs-wallet' {
}
/**
*
* @param icap
* @description create an instance based on a new random key (setting icap to true will generate an address suitable for the ICAP Direct mode
*/
*
* @param icap
* @description create an instance based on a new random key (setting icap to true will generate an address suitable for the ICAP Direct mode
*/
function generate(icap?: boolean): IFullWallet;
/**
*
* @param pattern
* @description create an instance where the address is valid against the supplied pattern (this will be very slow)
*/
*
* @param pattern
* @description create an instance where the address is valid against the supplied pattern (this will be very slow)
*/
function generateVanityAddress(pattern: string | RegExp): IFullWallet;
/**
* @description create an instance based on a raw private key
* @param input
*
*/
* @description create an instance based on a raw private key
* @param input
*
*/
function fromPrivateKey(input: Buffer): IFullWallet;
/**
* @description create an instance based on a BIP32 extended private key (xprv)
* @param input
*/
* @description create an instance based on a BIP32 extended private key (xprv)
* @param input
*/
function fromExtendedPrivateKey(input: Buffer): IFullWallet;
/**
* @description create an instance based on a public key (certain methods will not be available)
* @param input
* @param nonStrict
*/
* @description create an instance based on a public key (certain methods will not be available)
* @param input
* @param nonStrict
*/
function fromPublicKey(
input: Buffer,
nonStrict?: boolean
): IPublicKeyOnlyWallet;
/**
* @description create an instance based on a BIP32 extended public key (xpub)
* @param input
*/
* @description create an instance based on a BIP32 extended public key (xpub)
* @param input
*/
function fromExtendedPublicKey(input: string): IPublicKeyOnlyWallet;
/**
* @description import a wallet (Version 1 of the Ethereum wallet format)
* @param input
* @param password
*/
* @description import a wallet (Version 1 of the Ethereum wallet format)
* @param input
* @param password
*/
function fromV1(input: IV1Wallet | string, password: string): IFullWallet;
/**
* @description import a wallet (Version 3 of the Ethereum wallet format). Set nonStrict true to accept files with mixed-caps.
* @param input
* @param password
* @param nonStrict
*/
* @description import a wallet (Version 3 of the Ethereum wallet format). Set nonStrict true to accept files with mixed-caps.
* @param input
* @param password
* @param nonStrict
*/
function fromV3(
input: IV3Wallet | string,
password: string,
@ -324,10 +324,10 @@ declare module 'ethereumjs-wallet' {
): IFullWallet;
/**
* @description import an Ethereum Pre Sale wallet
* @param input
* @param password
*/
* @description import an Ethereum Pre Sale wallet
* @param input
* @param password
*/
function fromEthSale(
input: IPresaleWallet | string,
password: string

View File

@ -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
};
}