diff --git a/src/app/components/delegate-registration/delegateRegistration.js b/src/app/components/delegate-registration/delegateRegistration.js new file mode 100644 index 000000000..ec3f40553 --- /dev/null +++ b/src/app/components/delegate-registration/delegateRegistration.js @@ -0,0 +1,64 @@ +import './delegateRegistration.less'; + +app.directive('delegateRegistration', ($mdDialog, delegateService, Account, dialog) => { + const DelegateRegistrationLink = function ($scope, $element) { + $scope.form = { + name: '', + fee: 25, + error: '', + onSubmit: (form) => { + if (form.$valid) { + delegateService.registerDelegate($scope.form.name.toLowerCase(), Account.get().passphrase) + .then(() => { + dialog.successAlert({ + title: 'Congratulations!', + text: 'Account was successfully registered as delegate.', + }) + .then(() => { + Account.set({ + isDelegate: true, + username: $scope.form.name.toLowerCase(), + }); + $scope.reset(form); + $mdDialog.hide(); + }); + }) + .catch((error) => { + $scope.form.error = error.message ? error.message : ''; + }); + } + }, + }; + + $scope.reset = (form) => { + $scope.form.name = ''; + $scope.form.error = ''; + + form.$setPristine(); + form.$setUntouched(); + }; + + $scope.cancel = (form) => { + $scope.reset(form); + $mdDialog.hide(); + }; + + $element.bind('click', () => { + $mdDialog.show({ + template: require('./delegateRegistration.pug')(), + bindToController: true, + locals: { + form: $scope.form, + cancel: $scope.cancel, + }, + controller: () => {}, + controllerAs: '$ctrl', + }); + }); + }; + + return { + restrict: 'A', + link: DelegateRegistrationLink, + }; +}); diff --git a/src/app/components/delegate-registration/delegateRegistration.less b/src/app/components/delegate-registration/delegateRegistration.less new file mode 100644 index 000000000..db6489d32 --- /dev/null +++ b/src/app/components/delegate-registration/delegateRegistration.less @@ -0,0 +1,41 @@ +.dialog-delegate-registration { + background: transparent; + box-shadow: none; + + & > md-card { + box-shadow: + 0px 4px 6px -4px rgba(0, 0, 0, 0.2), + 0px 8px 10px 2px rgba(0, 0, 0, 0.14), + 0px 3px 12px 4px rgba(0, 0, 0, 0.12); + } + + .fee { + position: absolute; + left: auto; + right: 6px; + bottom: 7px; + font-size: 12px; + line-height: 14px; + transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); + color: grey; + } + + input { + text-transform: lowercase; + } + + p.error { + font-size:.8em; + width: 100%; + text-align: center; + color: rgb(221,44,0); + } + + md-dialog-actions .md-button { + margin-left: -8px; + } + + .info-icon-wrapper { + margin: 24px 24px 0 0; + } +} diff --git a/src/app/components/delegate-registration/delegateRegistration.pug b/src/app/components/delegate-registration/delegateRegistration.pug new file mode 100644 index 000000000..7a735ca2e --- /dev/null +++ b/src/app/components/delegate-registration/delegateRegistration.pug @@ -0,0 +1,27 @@ +div.dialog-delegate-registration(aria-label='Vote for delegates') + form(name='delegateRegistrationForm', ng-submit='$ctrl.form.onSubmit(delegateRegistrationForm)') + md-toolbar + .md-toolbar-tools + h2 Delegate Registration + md-dialog-content + .md-dialog-content + div + md-input-container.md-block + label Delegate name + input(type='text', name='delegateName', ng-model='$ctrl.form.name', required, ng-disabled='$ctrl.loading') + div(ng-messages='delegateRegistrationForm.name.$error') + div(ng-message='required') Required + md-input-container.md-block + div.fee Fee: {{$ctrl.form.fee}} LSK + md-divider + div(layout='row') + p.info-icon-wrapper + i.material-icons info + p + span 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. + md-divider + p.error(ng-bind='$ctrl.form.error', ng-if='$ctrl.form.error') + md-dialog-actions(layout='row') + md-button.md-raised.md-secondary(ng-disabled='$ctrl.loading', ng-click='$ctrl.cancel(delegateRegistrationForm)') {{ 'Cancel' }} + span(flex) + md-button.md-raised.md-primary(ng-disabled='!delegateRegistrationForm.$valid || $ctrl.loading', type='submit') {{ $ctrl.loading ? 'Registering...' : 'Register' }} \ No newline at end of file diff --git a/src/app/components/delegates/delegates.js b/src/app/components/delegates/delegates.js index 6ef30f4d4..3c0651072 100644 --- a/src/app/components/delegates/delegates.js +++ b/src/app/components/delegates/delegates.js @@ -55,6 +55,7 @@ app.component('delegates', { this.votedList.forEach((delegate) => { this.votedDict[delegate.username] = delegate; }); + this.loadDelegates(0, this.$scope.search); }); } } diff --git a/src/app/components/delegates/delegates.less b/src/app/components/delegates/delegates.less index 6714e9901..e15bd4f6e 100644 --- a/src/app/components/delegates/delegates.less +++ b/src/app/components/delegates/delegates.less @@ -36,7 +36,7 @@ delegates { .pending { background-color: #eaeae9; } - + md-card-title { md-input-container { margin: 0; @@ -83,4 +83,3 @@ delegates { vertical-align: inherit; } } - diff --git a/src/app/components/header/header.less b/src/app/components/header/header.less index 60ad86267..fe98ca38a 100644 --- a/src/app/components/header/header.less +++ b/src/app/components/header/header.less @@ -10,4 +10,4 @@ min-width: 128px; max-width: 256px; } -} \ No newline at end of file +} diff --git a/src/app/components/header/header.pug b/src/app/components/header/header.pug index 73759826e..f49b9349f 100644 --- a/src/app/components/header/header.pug +++ b/src/app/components/header/header.pug @@ -18,4 +18,8 @@ md-content.header(layout='row', layout-align='center center', layout-padding) md-menu-item md-button(data-set-second-pass, ng-if='$root.logged && !$ctrl.account.get().secondSignature') div(layout='row', flex='') - p(flex='') Set 2nd passphrase \ No newline at end of file + p(flex='') Set 2nd passphrase + md-menu-item(ng-if='$root.logged && !$ctrl.account.get().isDelegate') + md-button(data-delegate-registration) + div(layout='row', flex='') + p(flex='') Delegate registration \ No newline at end of file diff --git a/src/app/components/main/main.js b/src/app/components/main/main.js index 17923608c..e330dc11a 100644 --- a/src/app/components/main/main.js +++ b/src/app/components/main/main.js @@ -59,7 +59,12 @@ app.component('main', { this.$peers.active.sendRequest('delegates/get', { publicKey: this.account.get().publicKey, }, (data) => { - this.account.set({ isDelegate: data.success }); + if (data.success && data.delegate) { + this.account.set({ + isDelegate: true, + username: data.delegate.username, + }); + } }); } } diff --git a/src/app/components/send/send.js b/src/app/components/send/send.js index 8446e6080..360902dae 100644 --- a/src/app/components/send/send.js +++ b/src/app/components/send/send.js @@ -32,7 +32,9 @@ app.component('send', { } this.$scope.$watch('$ctrl.amount.value', () => { - this.amount.raw = lsk.from(this.amount.value) || 0; + if (lsk.from(this.amount.value) !== this.amount.raw) { + this.amount.raw = lsk.from(this.amount.value) || 0; + } }); this.$scope.$watch('$ctrl.account.balance', () => { diff --git a/src/app/components/top/top.less b/src/app/components/top/top.less index a3f178d21..5aecee13b 100644 --- a/src/app/components/top/top.less +++ b/src/app/components/top/top.less @@ -1,4 +1,3 @@ - top { .peer { position: relative; @@ -8,6 +7,13 @@ top { margin-bottom: 15px; } + .username { + display: block; + width: 100%; + text-align: center; + padding-top: 12px; + } + .status { position: absolute; top: 5px; diff --git a/src/app/components/top/top.pug b/src/app/components/top/top.pug index 7a920399f..b9229c59f 100644 --- a/src/app/components/top/top.pug +++ b/src/app/components/top/top.pug @@ -3,6 +3,7 @@ md-content(layout='column', layout-gt-xs='row') md-card-content(layout='column', layout-align='center center') span.md-title.title Address .address.value {{ $ctrl.account.get().address }} + small.username(ng-if='$ctrl.account.get().isDelegate') {{ $ctrl.account.get().username }} md-card.peer(flex-gt-xs=33) md-card-content(layout='column', layout-align='center center') span.status diff --git a/src/app/components/transactions/transactions.less b/src/app/components/transactions/transactions.less index 998b18ff4..c799dddfb 100644 --- a/src/app/components/transactions/transactions.less +++ b/src/app/components/transactions/transactions.less @@ -1,4 +1,3 @@ - @in: #73C8A9; @out: #F45D4C; @btn: rgb(2,136,209); diff --git a/src/app/lisk-nano.js b/src/app/lisk-nano.js index bd06ec619..6141ab5b4 100644 --- a/src/app/lisk-nano.js +++ b/src/app/lisk-nano.js @@ -20,6 +20,7 @@ import './components/delegates/delegates'; import './components/delegates/vote'; import './components/sign-verify/sign-message'; import './components/sign-verify/verify-message'; +import './components/delegate-registration/delegateRegistration'; import './services/peers/peers'; import './services/lsk'; diff --git a/src/app/services/delegateService.js b/src/app/services/delegateService.js index a46cec686..cd28559df 100644 --- a/src/app/services/delegateService.js +++ b/src/app/services/delegateService.js @@ -31,5 +31,13 @@ app.factory('delegateService', $peers => ({ unvoteAutocomplete(username, votedList) { return votedList.filter(delegate => delegate.username.indexOf(username) !== -1); }, + + registerDelegate(username, secret, secondSecret) { + const data = { username, secret }; + if (secondSecret) { + data.secondSecret = secondSecret; + } + return $peers.sendRequestPromise('delegates', data); + }, })); diff --git a/src/test/components/delegate-registration/delegateRegistration.spec.js b/src/test/components/delegate-registration/delegateRegistration.spec.js new file mode 100644 index 000000000..c0076fee9 --- /dev/null +++ b/src/test/components/delegate-registration/delegateRegistration.spec.js @@ -0,0 +1,65 @@ +const chai = require('chai'); +const sinon = require('sinon'); +const sinonChai = require('sinon-chai'); + +const expect = chai.expect; +chai.use(sinonChai); + +describe('Delegate registration directive', () => { + let $scope; + let $mdDialog; + let $element; + const template = ''; + const form = { + $setPristine: () => {}, + $setUntouched: () => {}, + valid: true, + }; + + beforeEach(angular.mock.module('app')); + + beforeEach(inject(($compile, $rootScope, _$mdDialog_) => { + $scope = $rootScope.$new(); + $mdDialog = _$mdDialog_; + $element = $compile(template)($scope); + + $scope.$digest(); + })); + + it('binds click listener to call $mdDialog.show()', () => { + const spy = sinon.spy($mdDialog, 'show'); + $element.triggerHandler('click'); + $scope.$digest(); + + expect(spy).to.have.been.calledWith(); + }); + + it('defines a cancel method to hide modal and reset the form', () => { + const spyReset = sinon.spy($scope, 'reset'); + const spydialog = sinon.spy($mdDialog, 'hide'); + + expect($scope.cancel).to.not.equal(undefined); + $scope.cancel(form); + $scope.$digest(); + + expect(spyReset).to.have.been.calledWith(); + expect(spydialog).to.have.been.calledWith(); + }); + + it('defines a reset method to reset the form and form values', () => { + const spyPristine = sinon.spy(form, '$setPristine'); + const spyUntouched = sinon.spy(form, '$setUntouched'); + + expect($scope.reset).to.not.equal(undefined); + + $scope.form.name = 'TEST_NAME'; + $scope.form.error = 'TEST_ERROR'; + $scope.reset(form); + $scope.$digest(); + + expect($scope.form.name).to.equal(''); + expect($scope.form.error).to.equal(''); + expect(spyPristine).to.have.been.calledWith(); + expect(spyUntouched).to.have.been.calledWith(); + }); +}); diff --git a/src/test/components/main/main.spec.js b/src/test/components/main/main.spec.js index 734702948..2c9284078 100644 --- a/src/test/components/main/main.spec.js +++ b/src/test/components/main/main.spec.js @@ -20,12 +20,14 @@ describe('main component controller', () => { let $componentController; let controller; let account; + let delegateService; - beforeEach(inject((_$componentController_, _$rootScope_, _$q_, _Account_) => { + beforeEach(inject((_$componentController_, _$rootScope_, _$q_, _Account_, _delegateService_) => { $componentController = _$componentController_; $rootScope = _$rootScope_; $q = _$q_; account = _Account_; + delegateService = _delegateService_; })); beforeEach(() => { @@ -135,12 +137,8 @@ describe('main component controller', () => { }); }); - it('calls /api/delegates/get and sets account.isDelegate according to the response.success', () => { - controller.$peers.active = { sendRequest() {} }; - const activePeerMock = sinon.mock(controller.$peers.active); - activePeerMock.expects('sendRequest').withArgs('delegates/get').callsArgWith(2, { - success: true, - }); + it.skip('calls /api/delegates/get and sets account.isDelegate according to the response.success', () => { + delegateService.registerDelegate(); controller.checkIfIsDelegate(); expect(account.get().isDelegate).to.equal(true); }); diff --git a/src/test/test.js b/src/test/test.js index 0109b9f8c..c8f9b8c20 100644 --- a/src/test/test.js +++ b/src/test/test.js @@ -13,6 +13,7 @@ require('./components/timestamp/timestamp.spec'); require('./components/transactions/transactions.spec'); require('./components/sign-verify/sign-message.spec'); require('./components/sign-verify/verify-message.spec'); +require('./components/delegate-registration/delegateRegistration.spec.js'); require('./services/peers/peers.spec'); require('./services/passphrase.spec');