With Android and iOS so obtainable and development so easy, people often forget the importance of desktop applications. I have a phone and tablet, but I depend on my desktop applications much more. However, the development of these applications are not difficult.
With frameworks like Electron, developers can create cross-platform desktop applications with JavaScript and HTML. Take the example I wrote about previously in the article titled, Create a Cross-Platform Desktop DigiByte DGB Wallet with Angular and Electron. I demonstrated how to leverage a very popular framework to create a cryptocurrency wallet.
DigiByte isn’t the only popular blockchain on the internet. I’ve also been looking into Stellar because of all the buzz it has been receiving from popular companies like Stripe.
We’re going to see how to create a Stellar XLM wallet for Mac, Windows, and Linux using Angular and Electron.
If you’ve been keeping up with my cryptocurrency development tutorials, we’re going to build something that looks a bit familiar. Take the following image for example:
We’re going to create an application that displays the XLM balance for a given address and the market value that it holds in United States Dollars (USD). Using the secret key, a public destination address, and an amount, we’ll create a signed transaction. Because we’re prototyping, we’re not actually going to send the transaction, but once we’ve created it, sending it is not difficult.
When it comes to developing with Electron, most of the work is with bundling a web application to be used on the desktop. For this reason, we’re going to develop an Angular application, and use Electron at the end.
The best and easiest way to get an Angular project up and running is with the Angular CLI. With the CLI installed, execute the following command:
ng new stellar-desktop-wallet
The above command will create a new project with all the appropriate Angular dependencies. However, we’ll need a few dependencies for working with Stellar and Electron.
We’re going to need the Stellar JavaScript SDK and Electron. Navigate into the Angular project and execute the following from the command line:
npm install stellar-sdk --save
npm install electron --save-dev
We won’t be using Electron within the code, so we can install it as a development dependency.
I’m not much of a CSS wizard, so we’re going to rely on Bootstrap when it comes to making our application look attractive to users.
Download Bootstrap, and copy the css, fonts, and js directories in your project’s src/assets directory.
Because Bootstrap is being downloaded as built CSS, and JavaScript, we need to import the files in the project’s src/index.html file rather than with the Node Package Manager (NPM).
Open the project’s src/index.html file and make it look like the following:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Stellar Desktop Wallet</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="/assets/css/bootstrap.min.css" />
</head>
<body>
<app-root></app-root>
</body>
</html>
Notice that we’ve just included the Bootstrap CSS file. Take not of what this file looks like because we’ll be coming back to change it later.
Now let’s start adding some application logic.
We’ll be editing a few different files within our project to make our application work.
Since we’ll be working with remote web services and forms, we need to import a few Angular modules that are already available, but not ready for use. Open the project’s src/app/app.module.ts file and include the following TypeScript code:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from "@angular/forms";
import { HttpModule } from "@angular/http";
import "rxjs/Rx";
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
In the above code, we’ve imported the FormsModule
, HttpModule
, and RxJS as well as added the modules to the imports
array of the @NgModule
block.
We won’t have navigation in our application, so we can focus on the core TypeScript logic and HTML.
Open the project’s src/app/app.component.ts file and include the following:
import { Component } from '@angular/core';
import { Http } from "@angular/http";
import { Observable } from "rxjs/Observable";
import * as StellarSDK from "stellar-sdk";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
private stellarServer: any;
public wallet: any;
public input: any;
public output: string;
public constructor(private http: Http) {}
public getWalletAndMarketValue() {}
public getWalletValue(address: string) {}
public getMarketValue() {}
public createTransaction() {}
}
In the above code, we left all the methods empty because we plan to go through them together.
The stellarServer
variable will be used for communicating with a public Stellar node. The wallet
variable will hold our wallet balance and market value. The input
variable will be bound to a form in the HTML and the output
variable will hold our signed transaction.
In the constructor
method, we can initialize a lot of our variables:
public constructor(private http: Http) {
StellarSDK.Network.usePublicNetwork();
this.stellarServer = new StellarSDK.Server("https://horizon.stellar.org");
this.wallet = {
xlmValue: 0,
usdValue: 0
};
this.input = {
secretKey: "",
recipient: "GANRQF6LV7N4SKTGSPOZQDPQPWI2VQ4DUFO2CT7MO24L4DGIC6U5RZBS",
amount: "10.1234567"
};
}
In the constructor
method we’re stating that we plan to use the public network and define the live-net node that we wish to use. Then we define each of the defaults for our public variables.
The Stellar servers, or any blockchain servers for example, generally don’t keep track of the market value. For this reason, we have to use an API like CoinMarketCap.
Take a look at the getMarketValue
method:
public getMarketValue() {
return this.http.get("https://api.coinmarketcap.com/v1/ticker/stellar/")
.map(result => result.json())
.map(result => result[0]);
}
Using RxJS, we can issue an HTTP request, convert the result to JSON, and return only the first result when subscribed to.
Getting the wallet balance is slightly different than getting the market value because we need to use the Stellar JavaScript SDK. Technically, we can use REST and HTTP for the job, but the SDK makes life a little easier.
The getWalletValue
method will look something like the following:
public getWalletValue(address: string) {
return Observable.fromPromise(this.stellarServer.accounts().accountId(address).call())
.map(result => (<any> result).balances)
.map(result => {
for(let i = 0; i < result.length; i++) {
if(result[i].asset_type == "native") {
return result[i];
}
}
});
}
The Stellar SDK uses promises by default which aren’t very friendly with how Angular does things with RxJS. Instead, we turn the promise into an observable. The promise itself is a Stellar account based on a public address. There is a lot of data returned, but we only want the balance, so we map it and return it when subscribed.
When we get information about our wallet, we generally always want the balance and the market value. This brings us to the getWalletAndMarketValue
method:
public getWalletAndMarketValue() {
let keypair = StellarSDK.Keypair.fromSecret(this.input.secretKey);
this.getWalletValue(keypair.publicKey())
.do(result => this.wallet.xlmValue = result.balance)
.switchMap(result => this.getMarketValue())
.subscribe(result => {
this.wallet.usdValue = (this.wallet.xlmValue * result.price_usd).toFixed(2);
})
}
The above command will take a secret key bound from the form and get both the wallet balance and market value using the two previously created methods. Each of the wallet balance and market values will be added to the wallet
variable so it can be bound to the screen. The switchMap
operator in the above code allows us to chain our observables together and run them in sequence.
This brings us to our final method, the createTransaction
method:
public createTransaction() {
let keypair = StellarSDK.Keypair.fromSecret(this.input.secretKey);
Observable.fromPromise(this.stellarServer.accounts().accountId(keypair.publicKey()).call())
.map(result => <any> result)
.map(result => {
let account = new StellarSDK.Account(keypair.publicKey(), result.sequence);
let transaction = new StellarSDK.TransactionBuilder(account)
.addOperation(StellarSDK.Operation.payment({
destination: this.input.recipient,
asset: StellarSDK.Asset.native(),
amount: this.input.amount
}))
.build();
transaction.sign(keypair);
return transaction;
})
.subscribe(result => {
this.output = result.toEnvelope().toXDR("base64");
});
}
There are a few things happening when it comes to creating a transaction. First we’re looking up an account so that we can have the correct sequence number. With the public address and sequence number, we can start building the transaction. Using the TransactionBuilder
we can provide a destination address, asset type, and amount, each of which are bound from an HTML form. Once the transaction is built, we can sign it with our secret key.
After subscribing to the observable for creating a transaction, we encode it and display it on the screen in the output
variable.
So what does the HTML look like? Open the project’s src/app/app.component.html file and include the following:
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="well">
<h2>XLM: {{ wallet.xlmValue }}</h2>
<p>USD: {{ wallet.usdValue }}</p>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<form>
<div class="form-group">
<label for="secretKey">Secret Key</label>
<div class="row">
<div class="col-sm-10">
<input type="text" class="form-control" [(ngModel)]="input.secretKey" name="secretKey" placeholder="Secret Key">
</div>
<div class="col-sm-2">
<button type="submit" (click)="getWalletAndMarketValue()" class="btn btn-default btn-block">Load</button>
</div>
</div>
</div>
<div class="form-group">
<label for="destinationAddress">Destination Address (Public)</label>
<input type="text" class="form-control" [(ngModel)]="input.recipient" name="destinationAddress" placeholder="Destination Address">
</div>
<div class="form-group">
<label for="amount">Amount</label>
<input type="text" class="form-control" [(ngModel)]="input.amount" name="amount" placeholder="Amount">
</div>
<button type="button" (click)="createTransaction()" class="btn btn-default">Send</button>
</form>
</div>
</div>
<div class="row" style="margin-top: 20px">
<div class="col-sm-12">
<div class="form-group">
<textarea [(ngModel)]="output" name="transaction" class="form-control" style="height: 120px"></textarea>
</div>
</div>
</div>
</div>
There is a lot of markup above, but most of it is Bootstrap related, not critical to our Angular or even Electron project.
To display our balance and market value, we have the following:
<div class="row">
<div class="col-md-12">
<div class="well">
<h2>XLM: {{ wallet.xlmValue }}</h2>
<p>USD: {{ wallet.usdValue }}</p>
</div>
</div>
</div>
We’re leveraging Angular to easily render our public variables.
In the core of our HTML, we have a bunch of HTML input elements, each with some kind of [(ngModel)]
binding. These are binding the form element to the public variable in our TypeScript file. There are two buttons on the screen, each with a (click)
event that calls a method in the TypeScript file.
Like I said, most of the HTML is Bootstrap preparation.
As of now, we should have a functional Stellar XLM web application built with Angular. However, the goal here was to get compatibility with Electron to make it desktop compatible.
There isn’t much that needs to be done to get our web application bundled as a desktop application. At the root of your Angular project, create an electron.js file. This file should include the following JavaScript code:
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: 650})
console.log(__dirname);
// and load the index.html of the app.
win.loadURL(url.format({
pathname: path.join(__dirname, 'dist/index.html'),
protocol: 'file:',
slashes: true
}))
win.setResizable(false);
// 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.
The above code was taken straight from the Electron Quick Start documentation if you haven’t figured it out. However, there is one slight difference:
win.loadURL(url.format({
pathname: path.join(__dirname, 'dist/index.html'),
protocol: 'file:',
slashes: true
}))
The entry file is going to be in the dist directory because we plan to build the Angular project before bundling it into Electron.
We’re not done yet though. We need to update our project’s src/index.html file:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Stellar Desktop Wallet</title>
<base href="./">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="assets/css/bootstrap.min.css" />
</head>
<body>
<app-root></app-root>
</body>
</html>
You might think that the above file is the same, but it is not. Electron might not play nice with absolute paths, so we have to use relative paths instead. For this reason, the following two lines changed:
<base href="./">
<link rel="stylesheet" href="assets/css/bootstrap.min.css" />
The base url now begins with a period, and the leading slash was removed from the stylesheet. Now we can alter the scripts in our project.
Open the project’s package.json file and alter the scripts
section:
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build --prod",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"electron": "npm run build; cp electron.js dist/; ./node_modules/.bin/electron ."
},
We’ve included an electron
script that will build the Angular project, copy the electron.js file into the distribution directory, and launch it with Electron. However, Electron won’t know what to run until we give it a main file.
Within the package.json file, include the following:
"main": "dist/electron.js",
At this point in time, the project should build and run without any problems. You’ll also notice that you don’t need to serve the project like a lot of tutorials demonstrate. This application is completely bundled and ready to go.
You just saw how to build a cross-platform application using Angular and Electron. This cross-platform desktop application allows you to interact with Stellar XLM coins by checking your wallet balance and create transactions that could be later submitted.
If you found this tutorial helpful and you’re interested in donating, GANRQF6LV7N4SKTGSPOZQDPQPWI2VQ4DUFO2CT7MO24L4DGIC6U5RZBS is my public XLM address.
If you’re holding XRP and DGB, you might check out some of my other tutorials. I wrote a tutorial titled, Create a Cross-Platform Desktop Ripple XRP Wallet with Vue.js and Electron for XRP, and a tutorial titled, Create a Cross-Platform Desktop DigiByte DGB Wallet with Angular and Electron for DGB.