Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Create A Cross-Platform Desktop Ripple XRP Wallet With Vue.js And Electron

TwitterFacebookRedditLinkedInHacker News

As you are probably aware, I’ve been interested in cryptocurrency and the blockchain lately. I haven’t necessarily been buying anything, but I’ve been doing a lot of watching as it has all been very interesting so far.

I decided, a few weeks ago, to purchase some Ripple (XRP) because it seems like it could do great things for the banking industry. When it came to keeping track of my XRP, there wasn’t really anything available. There is Ripple Wallet, but after having used it for a little bit, it seemed buggy. That got me thinking. What if I were to build my own wallet?

Ripple actually released a Node.js library called ripple-lib on GitHub which makes it very easy to interact with XRP. What makes things even better is that the cross-platform desktop application framework, Electron, uses Node.js.

We’re going to see how to create a cross-platform desktop application using Node.js, Vue.js, and Electron to keep track of our Ripple and how much our wallet is worth.

Before we jump into actually building something, let’s stop and think about what we plan on building. Take a look at the application screenshot below:

XRP Wallet with Electron and Vue.js

Using the Vue.js JavaScript framework and the Bootstrap CSS framework, we plan to create an attractive frontend application. While we could, the frontend won’t be doing any heavy lifting with the Ripple network. Instead, we’ll be using Node.js inside of Electron to make requests against a service.

If at any time during this tutorial you feel like you’d like to donate some XRP to me, my public address is rn7HJDaYaEcDJvcELQFbnGXAEo2YA1RLNc. While not necessary, I’d be very grateful of anything that came my way.

Creating a Vue.js Web Application with the Vue CLI

To get things started, we’re going to create a fresh Vue.js project. The easiest way to do this is through the Vue CLI. With the Vue CLI installed, execute the following:

vue init simple xrp-wallet

Notice that we’re scaffolding a basic project, not one with Webpack or similar. There is a lot of preparation work necessary when trying to use Webpack with Electron. It is a project best saved for another day.

We’re not quite ready to start developing our desktop application, but we need to get Bootstrap set up. Download Bootstrap and copy the files into a directory within the project called static. It won’t already exist, but go ahead and create it. You’ll also want to create a static/css/app.css file for our custom styles.

At this point in time your project structure should look like the following:

static
    css
        bootstrap.min.css
        app.css
        // ...
    fonts
        // ...
    js
        // ...
index.html

There are plenty of files that come with Bootstrap, but we’re only concerned with knowing about the bootstrap.min.css file. It will use various fonts, but we won’t need to know about it.

Open the project’s index.html file and make it look like the following:

<!DOCTYPE html>
<html>
    <head>
        <title>XRP Wallet</title>
        <link rel="stylesheet" href="static/css/bootstrap.min.css">
        <link rel="stylesheet" href="static/css/app.css">
        <script src="https://unpkg.com/vue"></script>
    </head>
    <body>
        <div id="app">
            <!-- ... -->
        </div>

        <script>
            // ...
        </script>
    </body>
</html>

What we’re really after in the above HTML markup is the imports of our CSS files. Basically ignore everything else that is going on for now. Just include the following lines:

<link rel="stylesheet" href="static/css/bootstrap.min.css">
<link rel="stylesheet" href="static/css/app.css">

Make sure you use relative paths when referencing files. Absolute paths will likely cause problems with how Electron renders things.

Integrating Electron in the Vue.js Web Application

At this point in time we should have a Vue.js application created that is configured to use Bootstrap as our CSS framework. Both Vue.js and Bootstrap cover half the technologies used. The other half will use Node.js.

To initialize our project to use Node.js, execute the following from the command line:

npm init -y

The next step is to get Electron into our new Node.js project. From the command line, execute the following command to install the necessary Electron package:

npm install electron --save-dev

Since we’ll be working with Electron as a local dependency rather than a global dependency, we should probably create a script for it.

Open the project’s package.json file and include the following:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "electron": "./node_modules/.bin/electron ."
},

Our project is now suitable for Node.js, Vue.js, and Electron development, but we need to create a logic file for our project. At the root of our project, create a main.js file with the following JavaScript:

const {app, BrowserWindow } = require('electron')
const path = require('path')
const url = require('url')

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win

function createWindow () {
    // Create the browser window.
    win = new BrowserWindow({width: 800, height: 600})

    // and load the index.html of the app.
    win.loadURL(url.format({
        pathname: path.join(__dirname, 'index.html'),
        protocol: 'file:',
        slashes: true
    }))

    // Emitted when the window is closed.
    win.on('closed', () => {
        // Dereference the window object, usually you would store windows
        // in an array if your app supports multi windows, this is the time
        // when you should delete the corresponding element.
        win = null
    })
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)

// Quit when all windows are closed.
app.on('window-all-closed', () => {
    // On macOS it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd + Q
    if (process.platform !== 'darwin') {
        app.quit()
    }
})

app.on('activate', () => {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (win === null) {
        createWindow()
    }
})


// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

If the above JavaScript looks familiar, it is because it was taken directly from the Electron Quick Start. The main.js file will have all of our “backend” Node.js logic and the index.html file will have all of our “frontend” Vue.js HTML markup and JavaScript code.

For a sanity check, our project should have the following structure:

static
    css
        bootstrap.min.css
        app.css
        // ...
    fonts
        // ...
    js
        // ...
index.html
main.js
package.json

Now we can worry about adding our Node.js logic which will do the heavy lifting when it comes to Ripple.

Communicating with the CoinMarketCap API and the Ripple Servers

When it comes to our application, we’ll be using the CoinMarketCap API to track the price of XRP and other metrics about it. We’ll be using ripple-lib to track information about our wallet.

To make this possible, we need a few Node.js dependencies. From the command line, execute the following:

npm install ripple-lib request --save

The request package will allow us to make HTTP requests to the CoinMarketCap API from Node.js. More information on making HTTP requests in Node.js can be found in a previous tutorial I wrote on the topic titled, Consume Remote API Data from within a Node.js Application.

Open the project’s main.js file so we can import and configure the dependencies:

const RippleAPI = require("ripple-lib").RippleAPI;
const Request = require("request");

With dependencies imported, we need to initialize them. In reality, we just need to define the Ripple servers for later use. This can be done by doing the following:

const api = new RippleAPI({server: 'wss://s1.ripple.com:443'});
const address = 'rn7HJDaYaEcDJvcELQFbnGXAEo2YA1RLNc';

The address variable is the public address of your XRP wallet. While the ripple-lib package could create an address for us, I already had one that I wanted to use.

At the bottom of your main.js file, we can create the functions that will get our wallet value and the market value of Ripple:

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

exports.address = address;

exports.getRippleWalletValue = () => {
    return new Promise((resolve, reject) => {
        api.connect().then(() => {
            api.getBalances(address).then(balances => {
                resolve(balances);
            }, error => {
                reject(error);
            });
        }, error => {
            reject(error);
        });
    });
}

exports.getRippleMarketValue = function() {
    return new Promise((resolve, reject) => {
        Request.get("https://api.coinmarketcap.com/v1/ticker/ripple/", (error, response, body) => {
            if(error) {
                reject(error);
            }
            resolve(JSON.parse(body));
        });
    });
}

By exporting these functions we’ll be able to access them from our Vue.js code. Our Vue.js code will call these functions and render the result in the HTML.

The full main.js file should look like the following:

const {app, BrowserWindow } = require('electron')
const path = require('path')
const url = require('url')
const RippleAPI = require("ripple-lib").RippleAPI;
const Request = require("request");

const api = new RippleAPI({server: 'wss://s1.ripple.com:443'});
const address = 'rn7HJDaYaEcDJvcELQFbnGXAEo2YA1RLNc';

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win

function createWindow () {
    // Create the browser window.
    win = new BrowserWindow({width: 800, height: 600})

    // and load the index.html of the app.
    win.loadURL(url.format({
        pathname: path.join(__dirname, 'index.html'),
        protocol: 'file:',
        slashes: true
    }))

    // Emitted when the window is closed.
    win.on('closed', () => {
        // Dereference the window object, usually you would store windows
        // in an array if your app supports multi windows, this is the time
        // when you should delete the corresponding element.
        win = null
    })
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)

// Quit when all windows are closed.
app.on('window-all-closed', () => {
    // On macOS it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd + Q
    if (process.platform !== 'darwin') {
        app.quit()
    }
})

app.on('activate', () => {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (win === null) {
        createWindow()
    }
})


// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

exports.address = address;

exports.getRippleWalletValue = () => {
    return new Promise((resolve, reject) => {
        api.connect().then(() => {
            api.getBalances(address).then(balances => {
                resolve(balances);
            }, error => {
                reject(error);
            });
        }, error => {
            reject(error);
        });
    });
}

exports.getRippleMarketValue = function() {
    return new Promise((resolve, reject) => {
        Request.get("https://api.coinmarketcap.com/v1/ticker/ripple/", (error, response, body) => {
            if(error) {
                reject(error);
            }
            resolve(JSON.parse(body));
        });
    });
}

For this particular XRP wallet application, the Node.js logic is quite minimal. Getting the market value and wallet value will allow us to accomplish quite a bit.

Now we can start to string things together with Vue.js and HTML.

Developing the Vue.js Logic and HTML UI

The client facing part of our application can be broken into three parts. We’ll have an HTML layer, a JavaScript layer, and a custom CSS layer that goes beyond Bootstrap.

We’re going to start with the JavaScript logic. Open the project’s index.html file and include the following within the <script> tags:

<script>
    const { remote } = require('electron');
    const mainProcess = remote.require('./main.js');

    var app = new Vue({
        el: '#app',
        data: {
            address: "",
            ripple_coin: {},
            ripple_wallet: {
                xrp: 0.00,
                usd: 0.00
            }
        },
        mounted() {
            this.address = mainProcess.address;
            mainProcess.getRippleMarketValue().then(result => {
                this.ripple_coin = result[0];
                mainProcess.getRippleWalletValue().then(result => {
                    this.ripple_wallet.xrp = result[0].value;
                    this.ripple_wallet.usd = (this.ripple_wallet.xrp * this.ripple_coin.price_usd).toFixed(2);
                });
            });
        },
        methods: { }
    });
</script>

The first thing we do in the above JavaScript is import a few things that will allow us to use the previously exported functions within the main.js file.

The data object is where we initialize the variables that we plan to use throughout the application. The address variable will map to the address that we had set in the main.js file. The ripple_coin will represent the market data for Ripple and the ripple_wallet variable will hold information obtained from the ripple-lib package.

When the mounted method is called when the application loads, a few things will happen. We’ll get the market value of XRP by calling the getRippleMarketValue function and when the promise resolves we’ll get the wallet value by calling the getRippleWalletValue function.

this.ripple_wallet.usd = (this.ripple_wallet.xrp * this.ripple_coin.price_usd).toFixed(2);

There is no way to directly determine the USD price of any number of XRP, so we manually calculate it. With all of our data loaded from the Node.js functions, we can worry about rendering it on the screen.

<div id="app">
    <nav class="navbar navbar-default navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">XRP Wallet</a>
            </div>
            <div id="navbar" class="navbar-collapse collapse">
                <ul class="nav navbar-nav navbar-right">
                    <li><a href="#">{{ address }}</a></li>
                </ul>
            </div>
        </div>
    </nav>

    <div class="container">
        <div class="row">
            <div class="col-md-12">
                <div class="well">
                    <h1 class="text-center">{{ ripple_wallet.usd }} USD / {{ ripple_wallet.xrp }} XRP</h1>
                </div>
            </div>
        </div>
        <div class="row" style="margin-top: 50px">
            <div class="col-md-12">
                <table class="table">
                    <thead>
                        <tr>
                            <th>XRP Price (USD)</th>
                            <th>Market Cap (USD)</th>
                            <th>1H Change (%)</th>
                            <th>24H Change (%)</th>
                            <th>7D Change (%)</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td>{{ ripple_coin.price_usd }}</td>
                            <td>{{ ripple_coin.market_cap_usd }}</td>
                            <td>{{ ripple_coin.percent_change_1h }}</td>
                            <td>{{ ripple_coin.percent_change_24h }}</td>
                            <td>{{ ripple_coin.percent_change_7d }}</td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</div>

<div id="footer">
    <div class="container">
        <p class="footer-block">
            2017 &copy; thepolyglotdeveloper.com
        </p>
    </div>
</div>

There is a lot of HTML above, but most of it is Bootstrap markup. If you’ve ever used Bootstrap, you’ll know how much HTML is necessary to do anything.

So what is important in the above HTML? Let’s start with the area that displays our current wallet value:

<div class="well">
    <h1 class="text-center">{{ ripple_wallet.usd }} USD / {{ ripple_wallet.xrp }} XRP</h1>
</div>

We’re just rendering what we’ve calculated on the screen. Likewise, we are just rendering variables in the table that follows. When it comes to Vue.js, the JavaScript does all the heavy lifting, while the HTML just does simple rendering.

The full index.html markup and code can be seen below:

<!DOCTYPE html>
<html>
    <head>
        <title>XRP Wallet</title>
        <link rel="stylesheet" href="static/css/bootstrap.min.css">
        <link rel="stylesheet" href="static/css/app.css">
        <script src="https://unpkg.com/vue"></script>
    </head>
    <body>
        <div id="app">
            <nav class="navbar navbar-default navbar-fixed-top">
                <div class="container">
                    <div class="navbar-header">
                        <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                            <span class="sr-only">Toggle navigation</span>
                            <span class="icon-bar"></span>
                            <span class="icon-bar"></span>
                            <span class="icon-bar"></span>
                        </button>
                        <a class="navbar-brand" href="#">XRP Wallet</a>
                    </div>
                    <div id="navbar" class="navbar-collapse collapse">
                        <ul class="nav navbar-nav navbar-right">
                            <li><a href="#">{{ address }}</a></li>
                        </ul>
                    </div>
                </div>
            </nav>

            <div class="container">
                <div class="row">
                    <div class="col-md-12">
                        <div class="well">
                            <h1 class="text-center">{{ ripple_wallet.usd }} USD / {{ ripple_wallet.xrp }} XRP</h1>
                        </div>
                    </div>
                </div>
                <div class="row" style="margin-top: 50px">
                    <div class="col-md-12">
                        <table class="table">
                            <thead>
                                <tr>
                                    <th>XRP Price (USD)</th>
                                    <th>Market Cap (USD)</th>
                                    <th>1H Change (%)</th>
                                    <th>24H Change (%)</th>
                                    <th>7D Change (%)</th>
                                </tr>
                            </thead>
                            <tbody>
                                <tr>
                                    <td>{{ ripple_coin.price_usd }}</td>
                                    <td>{{ ripple_coin.market_cap_usd }}</td>
                                    <td>{{ ripple_coin.percent_change_1h }}</td>
                                    <td>{{ ripple_coin.percent_change_24h }}</td>
                                    <td>{{ ripple_coin.percent_change_7d }}</td>
                                </tr>
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        </div>

        <div id="footer">
            <div class="container">
                <p class="footer-block">
                    2017 &copy; thepolyglotdeveloper.com
                </p>
            </div>
        </div>

        <script>
            const { remote } = require('electron');
            const mainProcess = remote.require('./main.js');
            var app = new Vue({
                el: '#app',
                data: {
                    address: "",
                    ripple_coin: {},
                    ripple_wallet: {
                        xrp: 0.00,
                        usd: 0.00
                    }
                },
                mounted() {
                    this.address = mainProcess.address;
                    mainProcess.getRippleMarketValue().then(result => {
                        this.ripple_coin = result[0];
                        mainProcess.getRippleWalletValue().then(result => {
                            this.ripple_wallet.xrp = result[0].value;
                            this.ripple_wallet.usd = (this.ripple_wallet.xrp * this.ripple_coin.price_usd).toFixed(2);
                        });
                    });
                },
                methods: { }
            });
        </script>
    </body>
</html>

Remember the custom CSS that I had mentioned? Open the project’s static/css/app.css file and include the following:

body {
    padding-top: 100px;
    margin-bottom: 40px;
}

#footer {
    bottom: 0;
    width: 100%;
    position: absolute;
    height: 40px;
    background-color: #f5f5f5;
}

.footer-block {
    margin: 10px 0;
}

The above CSS isn’t truly necessary, but it makes the application look a little more pleasant. We want our XRP wallet to look good and perform well.

Running the Application to See it in Action

With the project created, coded, and ready to go, let’s run it as an Electron application. Remember that script that we had created in our package.json file? Let’s run it.

From the command line, execute the following:

npm run electron

If everything went well, you should be able to see your XRP total and the value it represents in USD. We won’t package the application for distribution, but if you wanted to package it for deployment, it can easily be read about in the Electron documentation.

Conclusion

You just saw how to create a cross-platform desktop application with Vue.js and Electron. More specifically, you saw how to create your own Ripple (XRP) wallet, bringing you closer to learning about how cryptocurrencies and the blockchain work.

The ripple-lib package can do more than just get your balances. You can create wallets and issue transactions using it as well.

If you have XRP and would like to donate some, my address is rn7HJDaYaEcDJvcELQFbnGXAEo2YA1RLNc. I’d certainly be appreciative to anything I receive.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in C#, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Unity. Nic writes about his development experiences related to making web and mobile development easier to understand.