{
changeDepth,
receiveDepth,
accountKey,
+ accountInfo = {},
} = opts;
return {
@@ -49,6 +50,7 @@ export const setWallet = opts => {
changeDepth,
receiveDepth,
accountKey,
+ accountInfo
},
};
};
@@ -102,6 +104,7 @@ export const fetchWallet = () => async (dispatch, getState) => {
changeDepth: accountInfo.changeDepth,
receiveDepth: accountInfo.receiveDepth,
accountKey: accountInfo.accountKey,
+ accountInfo, // TODO: remove all the above crap and just pass account object
}));
};
diff --git a/app/ducks/walletReducer.js b/app/ducks/walletReducer.js
index 2c292c8df..07b63408e 100644
--- a/app/ducks/walletReducer.js
+++ b/app/ducks/walletReducer.js
@@ -82,6 +82,7 @@ export default function walletReducer(state = getInitialState(), {type, payload}
receiveDepth: payload.receiveDepth,
accountKey: payload.accountKey,
initialized: typeof payload.initialized === 'undefined' ? state.initialized : payload.initialized,
+ accountInfo: payload.accountInfo,
};
case SET_BALANCE:
return {
diff --git a/app/pages/Account/index.js b/app/pages/Account/index.js
index 186db3dbb..4bc9c5cd5 100644
--- a/app/pages/Account/index.js
+++ b/app/pages/Account/index.js
@@ -31,6 +31,7 @@ const analytics = aClientStub(() => require("electron").ipcRenderer);
isFetching: state.wallet.isFetching,
network: state.wallet.network,
hnsPrice: state.node.hnsPrice,
+ accountInfo: state.wallet.accountInfo,
}),
(dispatch) => ({
fetchWallet: () => dispatch(walletActions.fetchWallet()),
@@ -62,6 +63,7 @@ export default class Account extends Component {
finalizeMany: PropTypes.func.isRequired,
renewMany: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
+ accountInfo: PropTypes.object.isRequired,
};
static contextType = I18nContext;
@@ -86,6 +88,11 @@ export default class Account extends Component {
constructor(props) {
super(props);
this.updateStatsAndBalance = throttle(this.updateStatsAndBalance, 15000, { trailing: true });
+
+ if ( this.props.accountInfo.type === 'multisig'
+ && !this.props.accountInfo.initialized) {
+ this.props.history.push('/multisig');
+ }
}
componentDidMount() {
diff --git a/app/pages/App/index.js b/app/pages/App/index.js
index 107d803c7..2f9ed77cc 100644
--- a/app/pages/App/index.js
+++ b/app/pages/App/index.js
@@ -38,6 +38,7 @@ import Exchange from '../Exchange';
import SignMessage from "../SignMessage";
import VerifyMessage from "../VerifyMessage";
import {fetchLocale, initHip2, checkForUpdates} from "../../ducks/app";
+import Multisig from "../Multisig";
import {I18nContext} from "../../utils/i18n";
const connClient = cClientStub(() => require('electron').ipcRenderer);
const settingClient = sClientStub(() => require('electron').ipcRenderer);
@@ -251,6 +252,12 @@ class App extends Component {
path="/exchange"
render={this.routeRenderer(t('headingExchange'), Exchange, true)}
/>
+
>
diff --git a/app/pages/Multisig/index.js b/app/pages/Multisig/index.js
new file mode 100644
index 000000000..63c6f41a3
--- /dev/null
+++ b/app/pages/Multisig/index.js
@@ -0,0 +1,137 @@
+import React, { Component, Fragment } from "react";
+import PropTypes from "prop-types";
+import { withRouter } from "react-router";
+import { connect } from "react-redux";
+import {clientStub as wClientStub} from "../../background/wallet/client";
+import * as networks from "hsd/lib/protocol/networks";
+import { clientStub as aClientStub } from "../../background/analytics/client";
+import * as walletActions from "../../ducks/walletActions";
+import { showError, showSuccess } from "../../ducks/notifications";
+import * as nameActions from "../../ducks/names";
+import * as nodeActions from "../../ducks/node";
+import {HeaderItem, HeaderRow, Table, TableItem, TableRow} from "../../components/Table";
+import CopyButton from "../../components/CopyButton";
+
+// I'm getting pretty tired of creating brand new stylesheets
+// for every single view!
+import "../../components/NameClaimModal/name-claim-modal.scss";
+
+const walletClient = wClientStub(() => require('electron').ipcRenderer);
+
+
+@withRouter
+@connect(
+ (state) => ({
+ accountInfo: state.wallet.accountInfo,
+ wallet: state.wallet,
+ }),
+ (dispatch) => ({
+ fetchWallet: () => dispatch(walletActions.fetchWallet()),
+ })
+)
+export default class Multisig extends Component {
+ static propTypes = {
+ accountInfo: PropTypes.object.isRequired,
+ wallet: PropTypes.object.isRequired,
+ fetchWallet: PropTypes.func.isRequired,
+ };
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ return (
+
+
+ Multisig wallet policy: {this.props.accountInfo.m}-of-{this.props.accountInfo.n}
+
+
+
+ Signer
+ Account Key (xpub)
+
+
+
+ {this.props.wallet.wid} (me)
+
+ {this.props.accountInfo.accountKey}
+
+
+
+ {this.renderOtherSigners()}
+
+
+
+ );
+ }
+
+ renderOtherSigners() {
+ const rows = this.props.accountInfo.n - 1; // Our own key is already there
+ const keys = this.props.accountInfo.keys;
+ const out = [];
+
+ for (let i = 0; i < rows; i++) {
+ let key = keys[i];
+
+ if (!key) {
+ key =
+
+ }
+
+ out.push(
+
+ Signer #{i+2}
+
+ {key}
+
+
+ );
+ }
+
+ return out;
+ }
+}
+
+class KeyInput extends Component {
+ static propTypes = {
+ fetchWallet: PropTypes.func,
+ }
+
+ state = {
+ errorMessage: null,
+ }
+
+ async addKey(xpub) {
+ this.setState({errorMessage: null})
+ try {
+ await walletClient.addSharedKey('default', xpub); // account, xpub
+ await this.props.fetchWallet();
+ } catch (e) {console.log(e)
+ this.setState({errorMessage: e.message});
+ }
+ }
+
+ handleChange = () => {
+ this.setState({errorMessage: null});
+ }
+
+ render() {
+ return (
+
+
e.key === 'Enter' && this.addKey(e.target.value)}
+ placeholder="xpub..."
+ />
+
+ {this.state.errorMessage}
+
+
+ );
+ }
+}
diff --git a/app/pages/Onboarding/ConnectLedger/index.js b/app/pages/Onboarding/ConnectLedger/index.js
index 0224a655f..46c268783 100644
--- a/app/pages/Onboarding/ConnectLedger/index.js
+++ b/app/pages/Onboarding/ConnectLedger/index.js
@@ -82,7 +82,14 @@ class ConnectLedger extends React.Component {
// set a small timeout to clearly show that this is
// a two-phase process.
setTimeout(async () => {
- await walletClient.createNewWallet(this.props.walletName, this.props.passphrase, true, xpub);
+ await walletClient.createNewWallet(
+ this.props.walletName,
+ this.props.passphrase,
+ true, // is Ledger
+ xpub,
+ 1, // m
+ 1 // n
+ );
await this.props.completeInitialization(this.props.walletName, this.props.passphrase);
this.props.history.push('/account');
}, 2000);
diff --git a/app/pages/Onboarding/CreateMultisig/index.js b/app/pages/Onboarding/CreateMultisig/index.js
new file mode 100644
index 000000000..2fbbc851e
--- /dev/null
+++ b/app/pages/Onboarding/CreateMultisig/index.js
@@ -0,0 +1,86 @@
+import React from 'react';
+import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
+import PropTypes from 'prop-types';
+import { clientStub as lClientStub } from '../../../background/ledger/client';
+import walletClient from '../../../utils/walletClient';
+import { connect } from 'react-redux';
+import * as walletActions from '../../../ducks/walletActions';
+
+
+@withRouter
+@connect((state) => ({
+ network: state.wallet.network,
+}), (dispatch) => ({
+ completeInitialization: (name, passphrase) => dispatch(
+ walletActions.completeInitialization(name, passphrase)
+ ),
+}))
+export default class CreateMultisig extends React.Component {
+ static propTypes = {
+ walletName: PropTypes.string.isRequired,
+ passphrase: PropTypes.string.isRequired,
+ onBack: PropTypes.func.isRequired,
+ onCancel: PropTypes.func.isRequired,
+ completeInitialization: PropTypes.func.isRequired,
+ network: PropTypes.string.isRequired,
+ };
+
+ state = {
+ errorMessage: null,
+ m: 2,
+ n: 2
+ };
+
+ render() {
+ const {
+ walletName,
+ passphrase,
+ onBack,
+ onCancel,
+ completeInitialization,
+ network
+ } = this.props;
+
+ return (
+
+ Total number of participants:
+ this.setState({n: e.target.value})}
+ />
+ Required signatures per transaction:
+ this.setState({m: e.target.value})}
+ />
+
+
+ );
+ }
+}
diff --git a/app/pages/Onboarding/CreateNewAccount/index.js b/app/pages/Onboarding/CreateNewAccount/index.js
index 77710a4a1..29aefa345 100644
--- a/app/pages/Onboarding/CreateNewAccount/index.js
+++ b/app/pages/Onboarding/CreateNewAccount/index.js
@@ -9,6 +9,7 @@ import BackUpSeedWarning from '../BackUpSeedWarning/index';
import CopySeed from '../../CopySeed/index';
import ConfirmSeed from '../ConfirmSeed/index';
import SetName from '../SetName/index';
+import CreateMultisig from '../CreateMultisig/index';
import * as walletActions from '../../../ducks/walletActions';
import '../ImportSeedEnterMnemonic/importenter.scss';
import '../ImportSeedWarning/importwarning.scss';
@@ -27,6 +28,7 @@ const BACK_UP_SEED_WARNING = 4;
const COPY_SEEDPHRASE = 5;
const CONFIRM_SEEDPHRASE = 6;
const LEDGER_CONNECT = 7;
+const MULTISIG = 8;
class CreateNewAccount extends Component {
static propTypes = {
@@ -40,6 +42,7 @@ class CreateNewAccount extends Component {
state = {
currentStep: TERMS_OF_USE,
+ stepNumber: 1,
name: '',
seedphrase: '',
passphrase: '',
@@ -47,26 +50,46 @@ class CreateNewAccount extends Component {
};
render() {
- const totalSteps = this.props.match.params.loc === 'ledger' ? 4 : 6;
+ // null, 'ledger', 'multisig'
+ const variation = this.props.match.params.loc;
+
+ let totalSteps = 6;
+ switch (variation) {
+ case 'ledger':
+ totalSteps = 4;
+ break;
+ case 'multisig':
+ break;
+ }
switch (this.state.currentStep) {
case TERMS_OF_USE:
return (
this.setState({currentStep: SET_NAME})}
+ onAccept={() => this.setState({
+ currentStep: SET_NAME,
+ stepNumber: this.state.stepNumber + 1
+ })}
onBack={() => this.props.history.push('/funding-options')}
/>
);
case SET_NAME:
return (
this.setState({currentStep: TERMS_OF_USE})}
+ onBack={() => this.setState({
+ currentStep: TERMS_OF_USE,
+ stepNumber: this.state.stepNumber - 1
+ })}
onNext={(name) => {
- this.setState({currentStep: CREATE_PASSWORD, name});
+ this.setState({
+ currentStep: CREATE_PASSWORD,
+ name,
+ stepNumber: this.state.stepNumber + 1
+ });
}}
onCancel={() => this.props.history.push('/funding-options')}
/>
@@ -74,24 +97,36 @@ class CreateNewAccount extends Component {
case CREATE_PASSWORD:
return (
this.setState({
currentStep: SET_NAME,
+ stepNumber: this.state.stepNumber - 1
})
}
onNext={async (passphrase) => {
const optInState = await analytics.getOptIn();
- let currentStep =
- this.props.match.params.loc === 'ledger'
- ? LEDGER_CONNECT
- : BACK_UP_SEED_WARNING;
+
+ let currentStep = BACK_UP_SEED_WARNING;
+ switch (variation) {
+ case 'ledger':
+ currentStep = LEDGER_CONNECT;
+ break;
+ case 'multisig':
+ currentStep = MULTISIG;
+ break;
+ }
+
if (optInState === 'NOT_CHOSEN') {
currentStep = OPT_IN_ANALYTICS;
}
- this.setState({currentStep, passphrase});
+ this.setState({
+ currentStep,
+ passphrase,
+ stepNumber: this.state.stepNumber + 1
+ });
}}
onCancel={() => this.props.history.push('/funding-options')}
/>
@@ -99,28 +134,57 @@ class CreateNewAccount extends Component {
case OPT_IN_ANALYTICS:
return (
this.setState({currentStep: CREATE_PASSWORD})}
+ onBack={() => this.setState({
+ currentStep: CREATE_PASSWORD,
+ stepNumber: this.state.stepNumber - 1
+ })}
onNext={async (optInState) => {
await analytics.setOptIn(optInState);
+
+ let currentStep = BACK_UP_SEED_WARNING;
+ switch (variation) {
+ case 'ledger':
+ currentStep = LEDGER_CONNECT;
+ break;
+ case 'multisig':
+ currentStep = MULTISIG;
+ break;
+ }
+
this.setState({
- currentStep:
- this.props.match.params.loc === 'ledger'
- ? LEDGER_CONNECT
- : BACK_UP_SEED_WARNING,
+ currentStep,
+ stepNumber: this.state.stepNumber + 1
});
}}
onCancel={() => this.props.history.push('/funding-options')}
/>
);
+ case MULTISIG:
+ return(
+
+ this.setState({
+ currentStep: CREATE_PASSWORD,
+ stepNumber: this.state.stepNumber - 1
+ })
+ }
+ onCancel={() => this.props.history.push('/funding-options')}
+ />
+ );
case LEDGER_CONNECT:
return (
- this.setState({ currentStep: CREATE_PASSWORD })
+ this.setState({
+ currentStep: CREATE_PASSWORD,
+ stepNumber: this.state.stepNumber - 1
+ })
}
onCancel={() => this.props.history.push('/funding-options')}
/>
@@ -128,10 +192,13 @@ class CreateNewAccount extends Component {
case BACK_UP_SEED_WARNING:
return (
- this.setState({ currentStep: CREATE_PASSWORD })
+ this.setState({
+ currentStep: CREATE_PASSWORD,
+ stepNumber: this.state.stepNumber - 1
+ })
}
onNext={async () => {
this.setState({ isLoading: true });
@@ -145,14 +212,18 @@ class CreateNewAccount extends Component {
this.state.name,
this.state.passphrase,
false, // isLedger
- null // xpub (Ledger only)
+ null, // xpub (Ledger only)
+ 1, // m
+ 1 // n
);
}
const {phrase} = await walletClient.revealSeed(this.state.passphrase);
+
this.setState({
currentStep: COPY_SEEDPHRASE,
seedphrase: phrase,
isLoading: false,
+ stepNumber: this.state.stepNumber + 1
});
}}
onCancel={() => this.props.history.push('/funding-options')}
@@ -162,21 +233,30 @@ class CreateNewAccount extends Component {
case COPY_SEEDPHRASE:
return (
this.setState({ currentStep: BACK_UP_SEED_WARNING })}
- onNext={() => this.setState({ currentStep: CONFIRM_SEEDPHRASE })}
+ onBack={() => this.setState({
+ currentStep: BACK_UP_SEED_WARNING,
+ stepNumber: this.state.stepNumber - 1
+ })}
+ onNext={() => this.setState({
+ currentStep: CONFIRM_SEEDPHRASE,
+ stepNumber: this.state.stepNumber + 1
+ })}
onCancel={() => this.props.history.push('/funding-options')}
/>
);
case CONFIRM_SEEDPHRASE:
return (
this.setState({ currentStep: COPY_SEEDPHRASE })}
+ onBack={() => this.setState({
+ currentStep: COPY_SEEDPHRASE,
+ stepNumber: this.state.stepNumber - 1
+ })}
onNext={async () => {
await this.props.completeInitialization(
this.state.name,
diff --git a/app/pages/Onboarding/FundAccessOptions/index.js b/app/pages/Onboarding/FundAccessOptions/index.js
index 7e2af9887..e145970a0 100644
--- a/app/pages/Onboarding/FundAccessOptions/index.js
+++ b/app/pages/Onboarding/FundAccessOptions/index.js
@@ -51,6 +51,13 @@ class FundAccessOptions extends Component {
>
{t('obMainConnectLedger')}
+
{!!this.props.wallets.length && (