Get the Newsletter

Case Study: Stellarport

Posted by Ishai Strauss on April 22, 2019

Today, I'm pleased to introduce Ishai Strauss from Stellarport to talk about how they've built their product with Aurelia. Read on to hear from Ishai about Stellarport's experiences integrating Redux with Aurelia and leveraging decorator-based composition with Aurelia's Fetch client.

What is Stellarport?

Stellarport is a decentralized wallet and trading client, based on the Stellar network. Users can create and manage wallets that they have cryptographic control over as well as trade in real time directly to and from the wallet. Trading on Stellarport is all done via blockchain technology (using the Stellar protocol). While the backend of Stellarport is written in a variety of languages and frameworks, the front end is written in just one (our favorite) - Aurelia.

Why Aurelia?

We've been Aurelia fans for a long time, ever since the beta in fact. We'd already used Aurelia in production in conjunction with Redux with great success. Because Aurelia keeps it simple and focuses on doing its job simply, reliably, and in a modular way, plugging in Redux was easily done.

When beginning work on Stellarport, we knew that state management and reliability were going to be important factors to be taken seriously from the get go. It was an easy choice to use Aurelia + Redux.

Using Aurelia

Because Aurelia is so uncomplicated and modular, we've been able to customize it in a variety of very useful ways. Below, I will talk about some of the ways, we at Stellarport, have used Aurelia effectively.

Aurelia and Redux

As previously mentioned, one thing we focus on is state management. With a product like a real-time trading client, there is lots of data to keep track of and in sync. We wouldn't want different parts of the website showing different prices or market sizes for example. To ensure data synchronization, we store much of the data, especially in some of the more complicated parts of the website, in Redux.

Because Aurelia's view models are just classes, integrating Redux with Aurelia can be done by using a custom @connected decorator. Here is an example:

    
  import BigNumber from 'bignumber.js';
  import { computedFrom, bindable } from 'aurelia-framework';
  import { connected } from 'aurelia-redux-connect';
  
  export class OrderbookCustomElement {
      @bindable()
      currentTab;
  
      @connected('myAccount')
      account;
  
      @connected('exchange.assetPair')
      assetPair;
  
      @connected('exchange.orderbook')
      orderbook;
  
      @computedFrom('orderbook')
      get bids() {
          return this.orderbook ? this.orderbook.bids : [];
      }
  
      @computedFrom('orderbook')
      get asks() {
          return this.orderbook ? this.orderbook.asks : [];
      }
  
      @computedFrom('orderbook')
      get spread() {
          if (!this.orderbook || this.orderbook.bids.length === 0 || this.orderbook.asks.length === 0) {
              return null;
          }
  
          const topAsk = this.priceFromFraction(this.orderbook.asks[0]);
          const topBid = this.priceFromFraction(this.orderbook.bids[0]);
  
          return {
              raw: (new BigNumber(topAsk)).minus(topBid).toString(10),
              percent: (new BigNumber(topAsk)).dividedBy(topBid).minus(1).times(100).toString(10)
          };
      }
  
      priceFromFraction(order) {
          return order ? new BigNumber(order.priceNumerator).dividedBy(order.priceDenominator).toString(10) : '';
      }
  }
  
  

In the example, there are a few things to note:

  1. Notice how simple this view model looks. All the properties are simply getters. The view model is just the glue between the view and the model. The code is clear, maintainable, and reliable.
  2. We use the @connected decorator to connect the view model to Redux (passing in the object path required to access the desired value). The connected decorator will update the view model when the store changes.
  3. Because Redux is a state store, it is best to store the state in a fairly normalized way. This results in often needing values that are derived from the stored values, rather than the raw stored values. With Aurelia, this is easy with @computedFrom. Aurelia will automatically update @computedFrom bindings when the @connected dependencies update.

Our aurelia/redux integration is available on npm .

CRUD With Aurelia

Stellarport requires data from multiple data sources. One of Aurelia's strong points is its modular architecture. In our case, CRUD becomes easy with Aurelia's Fetch client. Because we are fetching data from multiple sources in different forms, we need multiple fetch clients each with a slightly different configuration. With Aurelia's DI, we can just inherit and then mixin functionality. Each inherited fetch client will be stored as a separate singleton inside of Aurelia's DI.

To do this we start with some decorators we can mixin to our clients. Let's start with a decorator that adds a baseUrl to a client:

    
  export function withBaseUrl(baseUrl) {
      return function(target) {
          const addBaseUrl = function(config) {
              return config
                  .withBaseUrl(baseUrl);
          };
  
          const previousModifyConfiguation = target.prototype.modifyConfiguration || function(config) { return config; };
  
          target.prototype.modifyConfiguration = function(config) {
              return addBaseUrl(
                  previousModifyConfiguation.call(this, config)
              );
          };
      };
  }
  
  

and then a decorator that adds the content type to the client's requests:

    
  export function sendJson() {
      return function(target) {
          const addContentTypeJsonHeader = function(config) {
              const defaults = config.defaults || {};
  
              return config
                  .withDefaults(
                      _merge(
                          defaults,
                          {
                              headers: {
                                  'content-type': 'application/json'
                              }
                          }
                      )
                  );
          };
  
          const previousModifyConfiguation = target.prototype.modifyConfiguration || function(config) { return config; };
  
          target.prototype.modifyConfiguration = function(config) {
              return addContentTypeJsonHeader(
                  previousModifyConfiguation.call(this, config)
              );
          };
      };
  }
  
  

Finally, let's add a (slightly more complicated) decorator that adds an authentication token to the request headers (JWT):

    
  export function withBearerToken() {
      return function(target) {
          if (!target.prototype.acquireToken) {
              throw new Error(target.name + ' is decorated with @withBearerToken() but does not have an implemented acquireToken method.');
          }
  
          const addBearerTokenInerceptor = function(context, config) {
              return config
                  .withInterceptor(bearerTokenInterceptorFactory(context));
          };
  
          const previousModifyConfiguation = target.prototype.modifyConfiguration || function(config) {return config;};
  
          target.prototype.modifyConfiguration = function(config) {
              return addBearerTokenInerceptor(
                  this,
                  previousModifyConfiguation.call(this, config)
              );
          };
      };
  }
  
  function bearerTokenInterceptorFactory(context) {
      return {
          request: function(request) {
              return context.acquireToken(request)
                  .then(token => {
                      if (token) {
                          request.headers.append('Authorization', 'Bearer ' + token);
                      }
                      return request;
                  })
                  .catch(err => {
                      if (context.acquireTokenError) {
                          context.acquireTokenError(err);
                      }
                      throw err;
                  });
          }.bind(this)
      };
  }
  
  

Now, we can use these to build up different HTTP clients:

    
  @withBaseUrl(window.stellarport.urls.aCoolAPI)
  @acceptAll()
  @sendJson()
  @withBearerToken()
  export class AuthCoolApiClient extends HttpClient {
      acquireToken() {
          ...
      }
  }
  
  @withBaseUrl(window.stellarport.urls.anotherAPI)
  @acceptAll()
  @sendJson()
  export class AnotherApiClient extends HttpClient {}
  
  

Finally, we initialize our api clients on app startup like so:

    
  authCoolApiClient.configure(config => authCoolApiClient.modifyConfiguration(config));
  anotherApiClient.configure(config => anotherApiClient.modifyConfiguration(config));
  
  

Here we have different API clients, with different base urls, one authenticated and one not. Aurelia makes it super easy.

Conclusion

Using Aurelia is a pleasure. It is simple, intuitive, and flexible. At Stellarport, we're sure we made the right choice building on Aurelia.