Skip to content
This repository has been archived by the owner on Apr 15, 2019. It is now read-only.

Register as delegate - Closes #354 #543

Merged
merged 13 commits into from
Aug 11, 2017
11 changes: 10 additions & 1 deletion src/components/header/headerElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import logo from '../../assets/images/LISK-nano.png';
import styles from './header.css';
import VerifyMessage from '../signVerify/verifyMessage';
import SignMessage from '../signVerify/signMessage';
import RegisterDelegate from '../registerDelegate';
import Send from '../send';
import PrivateWrapper from '../privateWrapper';
import SecondPassphraseMenu from '../secondPassphrase';
Expand All @@ -20,8 +21,16 @@ const HeaderElement = props => (
menuRipple
theme={styles}
>
{
!props.account.isDelegate &&
<MenuItem caption="Register as delegate"
onClick={() => props.setActiveDialog({
title: 'Register as delegate',
childComponent: RegisterDelegate,
})}
/>
}
<SecondPassphraseMenu />
<MenuItem caption="Register as delegate" />
<MenuItem caption="Sign message"
className='sign-message'
onClick={() => props.setActiveDialog({
Expand Down
3 changes: 2 additions & 1 deletion src/components/header/headerElement.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ describe('HeaderElement', () => {
beforeEach(() => {
const mockInputProps = {
setActiveDialog: () => { },
account: {},
};
propsMock = sinon.mock(mockInputProps);
wrapper = shallow(<HeaderElement setActiveDialog={mockInputProps.setActiveDialog} />);
wrapper = shallow(<HeaderElement {...mockInputProps} />);
});

afterEach(() => {
Expand Down
24 changes: 12 additions & 12 deletions src/components/passphrase/steps.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
export default ({ props, state, setState }) => ({
export default context => ({
info: {
cancelButton: {
title: 'cancel',
onClick: () => { props.closeDialog(); },
onClick: () => { context.props.closeDialog(); },
},
confirmButton: {
title: () => 'next',
fee: () => props.fee,
onClick: () => { setState({ current: 'generate' }); },
fee: () => context.props.fee,
onClick: () => { context.setState({ current: 'generate' }); },
},
},
generate: {
cancelButton: {
title: 'cancel',
onClick: () => { props.closeDialog(); },
onClick: () => { context.props.closeDialog(); },
},
confirmButton: {
title: () => 'Next',
Expand All @@ -24,26 +24,26 @@ export default ({ props, state, setState }) => ({
show: {
cancelButton: {
title: 'cancel',
onClick: () => { props.closeDialog(); },
onClick: () => { context.props.closeDialog(); },
},
confirmButton: {
title: () => 'Yes! It\'s safe',
fee: () => {},
onClick: () => { setState({ current: 'confirm' }); },
onClick: () => { context.setState({ current: 'confirm' }); },
},
},
confirm: {
cancelButton: {
title: 'Back',
onClick: () => { setState({ current: 'show' }); },
onClick: () => { context.setState({ current: 'show' }); },
},
confirmButton: {
title: () => (props.confirmButton || 'Login'),
title: () => (context.props.confirmButton || 'Login'),
fee: () => {},
onClick: () => {
props.onPassGenerated(state.passphrase);
if (!props.keepModal) {
props.closeDialog();
context.props.onPassGenerated(context.state.passphrase);
if (!context.props.keepModal) {
context.props.closeDialog();
}
},
},
Expand Down
22 changes: 22 additions & 0 deletions src/components/registerDelegate/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { connect } from 'react-redux';
import RegisterDelegate from './registerDelegate';
import { accountUpdated } from '../../actions/account';
import { transactionAdded } from '../../actions/transactions';
import { successAlertDialogDisplayed, errorAlertDialogDisplayed } from '../../actions/dialog';

const mapStateToProps = state => ({
account: state.account,
peers: state.peers,
});

const mapDispatchToProps = dispatch => ({
onAccountUpdated: data => dispatch(accountUpdated(data)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot find where is this used.

showSuccessAlert: data => dispatch(successAlertDialogDisplayed(data)),
showErrorAlert: data => dispatch(errorAlertDialogDisplayed(data)),
addTransaction: data => dispatch(transactionAdded(data)),
});

export default connect(
mapStateToProps,
mapDispatchToProps,
)(RegisterDelegate);
23 changes: 23 additions & 0 deletions src/components/registerDelegate/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import { expect } from 'chai';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import store from '../../store';
import RegisterDelegate from './index';

describe('RegisterDelegate HOC', () => {
let wrapper;

beforeEach(() => {
wrapper = mount(<Provider store={store}><RegisterDelegate closeDialog={() => {}} /></Provider>);
});

it('should render RegisterDelegate', () => {
expect(wrapper.find('RegisterDelegate')).to.have.lengthOf(1);
});

it('should mount registerDelegate with appropriate properties', () => {
const props = wrapper.find('RegisterDelegate').props();
expect(typeof props.closeDialog).to.be.equal('function');
});
});
90 changes: 90 additions & 0 deletions src/components/registerDelegate/registerDelegate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';
import Input from 'react-toolbox/lib/input';
import InfoParagraph from '../infoParagraph';
import ActionBar from '../actionBar';
import { registerDelegate } from '../../utils/api/delegate';
import Fees from '../../constants/fees';

class RegisterDelegate extends React.Component {
constructor() {
super();

this.state = {
name: '',
nameError: '',
};
}

changeHandler(name, value) {
this.setState({ [name]: value });
}

register(username, secondSecret) {
registerDelegate(this.props.peers.data, username,
this.props.account.passphrase, secondSecret)
.then((data) => {
this.props.showSuccessAlert({
text: `Delegate registration was successfully submitted with username: "${this.state.name}". It can take several seconds before it is processed.`,
});

// add to pending transaction
this.props.addTransaction({
id: data.transactionId,
senderPublicKey: this.props.account.publicKey,
senderId: this.props.account.address,
amount: 0,
fee: Fees.registerDelegate,
});
})
.catch((error) => {
if (error && error.message === 'Username already exists') {
this.setState({ nameError: error.message });
} else {
this.props.showErrorAlert({
text: error && error.message ? `${error.message}.` : 'An error occurred while registering as delegate.',
});
}
});
}

render() {
return (
<div>
<Input label='Delegate name' required={true}
autoFocus={true}
className='username'
onChange={this.changeHandler.bind(this, 'name')}
error={this.state.nameError}
value={this.state.name} />
{
this.props.account.secondSignature &&
<Input label='Second secret'
required={true}
className='second-secret'
onChange={this.changeHandler.bind(this, 'secondSecret')}
value={this.state.secondSecret} />
}
<hr/>
<InfoParagraph>
Becoming a delegate requires registration. You may choose your own
delegate name, which can be used to promote your delegate. Only the
top 101 delegates are eligible to forge. All fees are shared equally
between the top 101 delegates.
</InfoParagraph>
<ActionBar
secondaryButton={{
onClick: this.props.closeDialog.bind(this),
}}
primaryButton={{
label: 'Register',
fee: Fees.registerDelegate,
disabled: !this.state.name ||
(this.props.account.secondSignature && !this.state.secondSecret),
onClick: this.register.bind(this, this.state.name, this.state.secondSecret),
}} />
</div>
);
}
}

export default RegisterDelegate;
116 changes: 116 additions & 0 deletions src/components/registerDelegate/registerDelegate.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React from 'react';
import chai, { expect } from 'chai';
import { mount } from 'enzyme';
import chaiEnzyme from 'chai-enzyme';
import sinon from 'sinon';
import Lisk from 'lisk-js';
import { Provider } from 'react-redux';
import store from '../../store';
import RegisterDelegate from './registerDelegate';
import * as delegateApi from '../../utils/api/delegate';

chai.use(chaiEnzyme());

const normalAccount = {
isDelegate: false,
address: '16313739661670634666L',
balance: 1000e8,
};

const delegateAccount = {
isDelegate: true,
address: '16313739661670634666L',
balance: 1000e8,
delegate: {
username: 'lisk-nano',
},
};

const withSecondSecretAccount = {
address: '16313739661670634666L',
balance: 1000e8,
delegate: {
username: 'lisk-nano',
},
secondSignature: 1,
};

const props = {
peers: {
data: Lisk.api({
name: 'Custom Node',
custom: true,
address: 'http://localhost:4000',
testnet: true,
nethash: '198f2b61a8eb95fbeed58b8216780b68f697f26b849acf00c8c93bb9b24f783d',
}),
},
closeDialog: () => {},
onAccountUpdated: () => {},
showSuccessAlert: () => {},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can make this showSuccessAlert: sinon.spy() and then expect that it has been called when necessary.

Those tests ending with wrapper.find('.submit-button').simulate('click'); and not checking anything with expect.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that I'm not doing this either (in send.test.js). I tried to but the spy does not seem to be called.

showErrorAlert: () => {},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

};

const delegateProps = { ...props, account: delegateAccount };
const normalProps = { ...props, account: normalAccount };
const withSecondSecretProps = { ...props, account: withSecondSecretAccount };

describe('RegisterDelegate', () => {
let wrapper;
let delegateApiMock;

beforeEach(() => {
delegateApiMock = sinon.mock(delegateApi);
});

afterEach(() => {
delegateApiMock.verify();
delegateApiMock.restore();
});

describe('Ordinary account', () => {
beforeEach(() => {
wrapper = mount(<Provider store={store}><RegisterDelegate {...normalProps} /></Provider>);
});

it('renders an InfoParagraph components', () => {
expect(wrapper.find('InfoParagraph')).to.have.length(1);
});

it('renders one Input component for a normal account', () => {
expect(wrapper.find('Input')).to.have.length(1);
});

it.skip('allows register as delegate for a non delegate account', () => {
wrapper.find('.username input').simulate('change', { target: { value: 'sample_username' } });
expect(wrapper.find('.primary-button button').props().disabled).to.not.equal(true);
});
});

describe('Ordinary account with second secret', () => {
beforeEach(() => {
wrapper = mount(<Provider store={store}>
<RegisterDelegate {...withSecondSecretProps} /></Provider>);
});

it('renders two Input component for a an account with second secret', () => {
expect(wrapper.find('Input')).to.have.length(2);
});

it('allows register as delegate for a non delegate account with second secret', () => {
wrapper.find('.username input').simulate('change', { target: { value: 'sample_username' } });
wrapper.find('.second-secret input').simulate('change', { target: { value: 'sample phrase' } });
});
});

describe('Delegate account', () => {
beforeEach(() => {
wrapper = mount(<Provider store={store}><RegisterDelegate {...delegateProps} /></Provider>);
});

it('does not allow register as delegate for a delegate account', () => {
wrapper.find('.username input').simulate('change', { target: { value: 'sample_username' } });
expect(wrapper.find('.primary-button button').props().disabled).to.be.equal(true);
});
});
});
5 changes: 1 addition & 4 deletions src/utils/passphrase.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
import { expect } from 'chai';
import { generateSeed, generatePassphrase, isValidPassphrase } from './passphrase';

if (global._bitcore) delete global._bitcore;
const mnemonic = require('bitcore-mnemonic');

chai.use(sinonChai);

const randoms = [
0.35125316992864564, 0.6836880327771695, 0.05720201294124072, 0.7136064360838184,
0.7655709865481362, 0.9670469669099078, 0.6699998930954159, 0.4377283727720742,
Expand Down