Предисловие
NEM — социально-ориентированная криптовалюта второго поколения, с философией финансовой свободы, децентрализации и равенства возможностей.
NEM платформа предоставляет пользователям следующие виды кошельков для безопасного хранения токенов — XEM:
- NCC Standalone (Nem Comunity Client) — автономный клиент. Рекомендуется использовать, если Вы желаете поднять «cуперноду» или полноценный узел. Для установки и запуска необходимы хотя бы минимальные навыки использования терминала;
- Nano Wallet — легкий клиент. Не требует установки дополнительного программного обеспечения, скачивания блокчейна. Все запросы обслуживают удаленные узлы. Разработан при помощи JavaScript-фреймворка — AngularJS;
- Android NEM Wallet — мобильный кошелек для системы Android.
NCC в своем функционале содержит очень нужный для кошелька модуль — «Адресная книга», который отсутствует в Nano Wallet.
Статья покажет вам, как создать модуль — «Адресная книга», содержащий в себе следующий функционал:
- Создание контакта
- Редактирование контакта
- Удаление контакта
- Сортировка списка контактов
- Экспорт адресной книги
- Импорт адресной книги
Введение
Первое, с чем сталкивается разработчик-новичок, никогда не занимавшийся написанием модулей для Nano Wallet, это вопрос — «С чего же начать?».
И так, для начала проверьте, установлена ли в вашей системе платформа Node.js, если нет, то скачиваем и устанавливаем.
Это вам понадобиться для сборки проекта.
Далее скачиваем официальный репозиторий кошелька.
Для сборки проекта, открываем терминал, переходим в корневую директорию проекта и запускаем следующие команды:
> npm install
> npm gulp
Теперь всё готово, и можно приступить непосредственно к разработке самого модуля.
Разработка модуля
Модули кошелька хранятся в следующей директории:
NanoWallet/ src/ app/ modules/
Итак, назовем наш модуль без лишних хитростей — addressBook. Все модули кошелька имеют следующую структуру файлов:
modules/ addressBook/ addressBook.config.js addressBook.controller.js index.js addressBook.html
addressBook.config.js — прописываем конфигурацию модуля. Здесь необходимо указать:
- url — url-адрес, по которому модуль будет доступен;
- controller — название контролера, обслуживающего модуль;
- controllerAs — псевдоним контролера;
- templateUrl — путь к файлу, в котором содержится представление (html-код) модуля;
- title — название модуля.
AddressBookConfig($stateProvider) {
'ngInject';
$stateProvider
.state('app.addressBook', {
url: '/address-book',
controller: 'AddressBookCtrl',
controllerAs: '$ctrl',
templateUrl: 'modules/addressBook/addressBook.html',
title: 'Address book'
});
};
export default AddressBookConfig;
addressBook.controller.js — здесь программируем логику нашего модуля. Контролер модуля «Адресная книга» будет содержать следующие основные свойства и методы:
- contacts — список контактов, хранящихся в локальном хранилище браузера;
- addContact(), add() — добавление в адресную книгу нового контакта;
- editContact(), edit() — редактирование контакта;
- removeContact(), remove() — удаление контакта;
- sortBy() -сортировка списка контактов;
- exportAddressBook() — экспорт экспорт й книги;
- importAddressBook() — импорт адресной книги;
import helpers from '../../utils/helpers';
import CryptoHelpers from '../../utils/CryptoHelpers';
class AddressBookCtrl {
constructor($localStorage, DataBridge, Wallet, Alert, $location, $filter, $q, $rootScope) {
'ngInject';
this.contacts = this._storage.contacts;
}
sortBy(propertyName) { // code };
addContact() { // code }
add() { // code }
editContact(elem) { // code }
edit() { // code }
removeContact(elem) { // code }
remove() { // code }
saveAddressBook() { // code }
exportAddressBook() { // code }
uploadAddressBook() { // code }
importAddressBook($fileContent) { // code }
transferTransaction(address) { // code }
}
export default AddressBookCtrl;
index.js — в этом файле создаем экземпляр нашего модуля с заранее определенными конфигурацией и контролером.
import angular from 'angular';
// Create the module where our functionality can attach to
let addressBookModule = angular.module('app.addressBook', []);
// Include our UI-Router config settings
import AddressBookConfig from './addressBook.config';
addressBookModule.config(AddressBookConfig);
// Controllers
import AddressBookCtrl from './addressBook.controller';
addressBookModule.controller('AddressBookCtrl', AddressBookCtrl);
export default addressBookModule;
addressBook.html — в этом файле делаем html-разметку модуля.
Сначала сделаем разметку бокового меню:
<div class="col-sm-3 sidebar" style="margin-top: 10px;">
<div class="panel panel-default">
<div class="panel-heading" style="background-color: rgb(68, 68, 68); color: white;border-radius: 0px;">
<i class="fa fa-chevron-right">i>
<span>{{ 'ADDRESS_BOOK_NAVIGATION' | translate }}span>
div>
<div class="panel-body">
<ul class="nav nav-pills nav-stacked">
<li>
<button type="button" class="btn btn-operations" ng-click="$ctrl.addContact()">{{ 'ADDRESS_BOOK_NEW' | translate }}button>
li>
<li>
<button type="button" ng-disabled="!$ctrl.contacts.items.length" class="btn btn-operations" ng-click="$ctrl.exportAddressBook()">{{ 'ADDRESS_BOOK_EXPORT_BTN' | translate }}button>
<a id="exportAddressBook" class="hidden" target="_blank">a>
li>
<li>
<button type="button" class="btn btn-operations" ng-click="$ctrl.uploadAddressBook()">{{ 'ADDRESS_BOOK_IMPORT_BTN' | translate }}button>
<input id="uploadAddressBook" accept=".adb" style="visibility:hidden;position:absolute;" import-address-book-file="$ctrl.importAddressBook($fileContent)" type="file">
li>
ul>
div>
div>
div>
Далее добавим разметку для вывода списка контактов и кнопок управления контактами:
<div class="col-sm-9 noPaddingLeft" style="margin-top: 10px;">
<div class="panel panel-default">
<div class="panel-heading" style="background-color: rgb(68, 68, 68); color: white;border-radius: 0px;">
<i class="fa fa-group">i> {{ 'ADDRESS_BOOK_LIST' | translate }}
<div style="float: right; position: relative; display: block;" ng-show="$ctrl.contacts.items.length > $ctrl.pageSize"><button class="buttonStyle" ng-disabled="$ctrl.currentPage == 0" ng-click="$ctrl.currentPage = $ctrl.currentPage-1" style="background-color: transparent; border: medium none;"><span class="fa fa-chevron-left" aria-hidden="true">span>button><b>{{$ctrl.currentPage+1}}/{{$ctrl.numberOfPages()}}b><button class="buttonStyle" ng-disabled="$ctrl.currentPage+1 >= $ctrl.numberOfPages()" ng-click="$ctrl.currentPage = $ctrl.currentPage+1" style="background-color: transparent; border: medium none;"> <span class="fa fa-chevron-right" aria-hidden="true">span>button>div>
div>
<table class="table table-bordered table-hover table-striped table-condensed" style="border:1px solid #444;table-layout:fixed;word-break:break-all;">
<thead style="color:white;">
<tr>
<th style="width: 20%;">
<a href="javascript:;" ng-click="$ctrl.sortBy('label')">
<i class="fa" ng-show="$ctrl.propertyName === 'label'" ng-class="{'fa-caret-down': $ctrl.revers, 'fa-caret-up': !$ctrl.revers}">i> {{ 'ADDRESS_BOOK_CONTACT_LABEL' | translate }}
a>
th>
<th>
<a href="javascript:;" ng-click="$ctrl.sortBy('address')">
<i class="fa" ng-show="$ctrl.propertyName === 'address'" ng-class="{'fa-caret-down': $ctrl.revers, 'fa-caret-up': !$ctrl.revers}">i> {{ 'ADDRESS_BOOK_ACCOUNT_ADDRESS' | translate }}
a>
th>
<th style="width: 200px;">{{ 'ADDRESS_BOOK_ACTIONS' | translate }}th>
tr>
thead>
<tbody style="background-color:white;text-align:center">
<tr ng-repeat="contact in $ctrl.contacts.items | orderBy:$ctrl.propertyName:$ctrl.revers | startFrom:$ctrl.currentPage*$ctrl.pageSize | limitTo:$ctrl.pageSize">
<td style="vertical-align: middle;border-bottom: 1px solid #444">{{contact.label}}td>
<td style="vertical-align: middle;border-bottom: 1px solid #444">{{contact.address}}td>
<td class="actions" style="vertical-align: middle;border-bottom: 1px solid #444;white-space:nowrap;">
<button type="button" class="btn btn-xs" ng-click="$ctrl.transferTransaction(contact.address)">Send XEMbutton>
<button type="button" class="btn btn-xs" ng-click="$ctrl.editContact(contact)">Editbutton>
<button type="button" class="btn btn-xs" ng-click="$ctrl.removeContact(contact)">Removebutton>
td>
tr>
tbody>
table>
<div class="panel-body" ng-show="!$ctrl.contacts.items.length" style="border: 1px solid #444;">
<p style="margin:0;">{{ 'GENERAL_NO_RESULTS' | translate }}p>
div>
<div class="panel-footer text-center" style="background-color: #e3e0cf; color: #444;padding:0;">
<small><b><i>{{ 'ADDRESS_BOOK_MAX_NUMBER' | translate }} {{ $ctrl.pageSize }}i>b>small>
div>
div>
div>
И добавим модальные окна, позволяющие добавлять/редактировать/удалять контакты:
<div id="contactModal" class="modal fade" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">×button>
<h4 class="modal-title" ng-show="!$ctrl.is_edit">{{ 'ADDRESS_BOOK_NEW' | translate }}h4>
<h4 class="modal-title" ng-show="$ctrl.is_edit">{{ 'ADDRESS_BOOK_EDIT' | translate }}h4>
div>
<div class="modal-body">
<fieldset class="form-group">
<input class="form-control form-control-lg"
type="password"
placeholder="{{ 'FORM_PASSWORD_FIELD_PLACEHOLDER' | translate }}"
ng-model="$ctrl.common.password"/>
fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg"
type="text"
placeholder="{{ 'ADDRESS_BOOK_CONTACT_LABEL' | translate }}"
ng-model="$ctrl.formData.label"/>
fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg"
type="text"
placeholder="{{ 'FORM_ADDRESS_FIELD_PLACEHOLDER' | translate }}"
ng-model="$ctrl.formData.address"/>
fieldset>
<button class="btn btn-success" style="border-radius:0px;"
type="submit" ng-show="!$ctrl.is_edit" ng-disabled="$ctrl.okPressed || !$ctrl.common.password.length || !$ctrl.formData.label || !$ctrl.formData.address" ng-click="$ctrl.add()">
<i class="fa fa-plus">i> {{ 'ADDRESS_BOOK_NEW_BTN' | translate }}
button>
<button class="btn btn-success" style="border-radius:0px;"
type="submit" ng-show="$ctrl.is_edit" ng-disabled="$ctrl.okPressed || !$ctrl.common.password.length || !$ctrl.formData.label || !$ctrl.formData.address" ng-click="$ctrl.edit()">
<i class="fa fa-edit">i> {{ 'ADDRESS_BOOK_EDIT_BTN' | translate }}
button>
div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'GENERAL_CLOSE' | translate }}button>
div>
div>
div>
div>
<div id="removeContactModal" class="modal fade" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">×button>
<h4 class="modal-title">{{ 'ADDRESS_BOOK_REMOVE' | translate }}h4>
div>
<div class="modal-body">
<fieldset class="form-group">
<input class="form-control form-control-lg"
type="password"
placeholder="{{ 'FORM_PASSWORD_FIELD_PLACEHOLDER' | translate }}"
ng-model="$ctrl.common.password"/>
fieldset>
<button class="btn btn-danger" style="border-radius:0px;" type="submit" ng-disabled="$ctrl.okPressed || !$ctrl.common.password.length" ng-click="$ctrl.remove()">
<i class="fa fa-remove">i> {{ 'ADDRESS_BOOK_REMOVE_BTN' | translate }}
button>
div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'GENERAL_CLOSE' | translate }}button>
div>
div>
div>
div>
Нам осталось написать стили для модуля. Основной css код кошелька содержится в файле:
NanoWallet/ src/ css/ nano.css
Стили для нашего модуля:
.address-book-page .panel-default .panel-heading {
border-bottom: 3px solid #dfa82f;
}
.address-book-page .sidebar .nav li .btn-operations {
width: 100%;
border-top: 1px solid #444;
border-bottom: 1px solid #444;
border-radius: 0;
margin-top: 7px;
color: #444;
background-color: #e3e0cf;
}
.address-book-page .sidebar .nav li:first-child .btn-operations { margin-top: 0; }
.address-book-page .sidebar .nav li .btn-operations:hover { background-color: #dad6bf; }
.address-book-page .sidebar .nav li .btn-operations:focus { outline: none; }
.address-book-page .sidebar .panel-body { padding: 10px 0; }
.address-book-page table th a { color: #FFF; }
.address-book-page table th a:hover,
.address-book-page table th a:focus,
.address-book-page table th a:active { color: #FFF; text-decoration: none; }
.address-book-page table .actions .btn { border: 1px solid rgba(153, 153, 153, 0.52); background: #E3E0CF; }
.address-book-page table .actions .btn:hover,
.address-book-page table .actions .btn:focus,
#addressBookModal table .btn:hover,
#addressBookModal table .btn:focus {
background-color: #dad6bf !important;
text-decoration: none;
}
#addressBookModal .modal-body { padding: 0; }
#addressBookModal .modal-footer { background: #444; }
#addressBookModal .modal-footer .btn {
border-radius: 3px;
background-color: #FFF;
color: #333;
border: none;
}
#addressBookModal .modal-footer .custom-pagination { color: #FFF; margin-top: 5px; }
#addressBookModal .modal-footer .custom-pagination button { opacity: 1; transition: .3s ease opacity; }
#addressBookModal .modal-footer .custom-pagination button[disabled="disabled"] { opacity: 0.3; }
#addressBookModal table th,
#addressBookModal table td { padding-left: 15px; padding-right: 15px; }
#addressBookModal table th:first-child,
#addressBookModal table td:first-child { border-right: 1px solid #DDDDDD; }
#addressBookModal table tr:first-child td { padding-top: 10px; }
#addressBookModal table tr:last-child td { padding-bottom: 15px; }
#addressBookModal table th { border-bottom: none; }
На этом разработка модуля окончена.
Теперь необходимо импортировать созданный модуль в приложение. Открываем следующий файл:
NanoWallet/ src/ app/ app.js
Прописываем следующий код, чтобы импортировать наш модуль:
<// Import our app modules
import './modules/addressBook';
// Create and bootstrap application
const requires = [
'app.addressBook'
];
Заключение
Вот собственно и получился готовый модуль.
Как мы увидели, разработка модулей для кошелька Nano Wallet не такое уж и сложное дело. Надеюсь, эта статья кому-нибудь поможет.
Удачи в разработке собственных модулей!