read

Introduction

NEM is a next generation blockchain platform that is ideal for being used socially. As with any social platform, an address book is essential to keep track of all of one’s contacts. This necessity is why I built an address book for NEM’s NanoWallet.

The NEM platform provides users with the following types of wallets for safe storage of tokens, which can either be XEM (the main currency on the NEM blockchain) or other mosaic assets.

  • NCC Standalone (NEM Community Client) — This is the standalone client. It is recommended to use this if you want to make a "supernode" or are running a full node. To install and run the standalone version, you will at least need to know how to use the terminal.
  • Nano Wallet — This is the lightweight and remote client. With the NanoWallet, there is no need to install additional software or download and sync a blockchain. Due to the nature of NEM, all requests to the blockchain are securely serviced through supernodes. The NanoWallet was written in JavaScript, allowing it to run on all platforms.
  • Android NEM Wallet - The best and only mobile wallet for the NEM platform featuring transaction notifications, multisignature support, and much more. (iOS coming soon)

NCC Standalone (nis-ncc-0.6.xx.tgz), the original NEM wallet, includes an address book. The NanoWallet did not have one. Part of the beauty of the NanoWallet is that any developer can add new modules, which is exactly what I have done here.

This article will show you how I created the Address Book module in NanoWallet. Here’s what the module includes.

  • Creating contacts
  • Editing contacts
  • Removing contacts
  • Sorting list contacts
  • Exporting a list of contacts
  • Importing a list of contacts

Getting Started

The first thing a developer wonders if they haven't written a module for the NanoWallet is how to start.

One great resource for learning how to build NanoWallet modules is a post from Quantum_Mechanics on the forum. If you’re having any issues writing a NanoWallet module, it may be helpful to refer to it.

First, check to see if you have installed Node.js on your system, and if not, download and install it. It is required to build the NanoWallet.

Next, download the source code of the NanoWallet from the official repository on GitHub.

To build the project, open a terminal, go to the root project directory and run the following commands:

npm install -g gulp-cli  
npm install  
gulp  

Now everything is ready, and you can proceed directly to the development of the module itself.

Module Development

Wallet modules are stored in the following directory:

NanoWallet/  
  src/
    app/
      modules/

Our module will be called “Address Book.” All module files have the following directory structure:

modules/  
  addressBook/
    addressBook.config.js
    addressBook.controller.js
    index.js
    addressBook.html

These files may be confusing in what they do at first, so here’s a rundown of what each file does.

addressBook.config.js - prescribes module configuration. This file is where you specify the following.

  • url - url-address where the module will be available;
  • controller - the controller name, the service module;
  • controllerAs - alias the controller;
  • templateUrl - path to the file, which contains the presentation (html-code) modules;
  • title - the name of the module.
function 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 - contains the logic module. Controller module «Address Book» will contain the following properties and methods:

  • contacts - the contacts list stored in the browser's local storage;
  • addContact (), add () - add to your address book new contact;
  • editContact (), edit () - editing a contact;
  • removeContact (), remove () - delete a contact;
  • sortBy () - sort contact list;
  • exportAddressBook () - export your contact list;
  • importAddressBook() — import your contact list.
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 - This file we create an instance of our module with a predefined configuration and controller, given in another file.

javascript  
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 - This file contains the HTML markup used to display the Address Book interface.

http  
<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>  

Next, add HTML to display the contact list and contact management buttons:

<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 XEM</button>
                        <button type="button" class="btn btn-xs" ng-click="$ctrl.editContact(contact)">Edit</button>
                        <button type="button" class="btn btn-xs" ng-click="$ctrl.removeContact(contact)">Remove</button>
                    </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>  

This adds modal windows which allows you to add, edit and delete contacts:

<!-- Add new and edit contact modal -->  
<div id="contactModal" class="modal fade" role="dialog">  
    <div class="modal-dialog">
        <!-- Modal content-->
        <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>

<!-- Remove contact modal -->  
<div id="removeContactModal" class="modal fade" role="dialog">  
    <div class="modal-dialog">
        <!-- Modal content-->
        <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>  

We still have to write the styles for the module. The main CSS wallet code is contained in the file:

NanoWallet/  
  src/
    css/
      nano.css

CSS is used to provide a style for the module as such.

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

To import the module created in the application, open the following file:

NanoWallet/  
  src/
    app/
      app.js

This contains the following code to import into our module.

// Import our app modules
import './modules/addressBook';

// Create and bootstrap application
const requires = [  
    'app.addressBook'
];

Сonclusion

As we have seen, the development of modules for the NanoWallet is not too difficult of a task. I hope this article helps someone with creating their own NanoWallet module.

Have fun designing your own modules! :)

The NEM team would like to thank Telegram user Delliant for writing this blog.

Blog Logo

A Nember


Published


Image

NEM

Official Blog of NEM/XEM

Back to Overview