Skip to content

v1 Architectural Description

Piyal Basu edited this page Jun 29, 2020 · 2 revisions

This is a description of the thoughts behind the setup of the codebase for version 1.

The 3 parts

As with any Chrome extension, Lyra is constructed of 3 main parts: Background, Popup, and Content Script. Because of this, each one has its own folder in the src/ folder.

Background is a script that is run by the extension on browser load, and due to our configuration, will run until the browser is closed. It is run in a headless environment by the extension, so it has access to all Web API's.

Popup is the UI for the extension itself.

Content Script is a script that runs when a new web page is opened. This is the "gateway" from the page to the extension.

Background serves as the backend, while the Popup and the Content Script are the user facing components. Content Script and Popup will be requesting from and delivering to Background, and are the only components with the ability to do this.

Background

The Background is encrypting and storing sensitive user information and also making requests to Horizon. The idea is for Content Script and Popup to have access a simple Promise-based API to get the data they need, much like a traditional RESTful API. The user facing components can call a method like createAccount and simply return the data needed; Background will handle the work of creating, funding, and encrypting the account. Another bonus to this type of structure is that sensitive data can be stored in a more secure way. Private keys, for example, will never available to the client.

Message Listener

Chrome extensions have a built in "message listener" that is structured much like a DOM event listener. Messages are sent to Background using chrome.runtime.sendMessage(). Content Script and Popup both are able to directly send messages to Background as they are inside the extension. Any entities outside of these 2 cannot send messages to our Background directly, a distinction we will explore later.

A message sent by chrome.runtime.sendMessage() takes the message to be sent as the first param and the response from background as the second. In Background, all of these messages are caught by chrome.runtime.onMessage.addListener, which must know how to properly handle each distinct message it receives. To distinguish between, say, a message requesting to create account versus a message requesting to sign a transaction, every message is required to have a type. The message type is a string, and works similar to an Action type in Redux. Also similar to Redux Actions, we save all types as constants in the Background folder. In "message listener", we are now able to check the type of the request, and then react accordingly, similar to a reducer.

The message listener returns a function that has a param called sendResponse. Once we finish with whatever action we are completing, we call this function with the data we need to return to the user. This response is handed back to chrome.runtime.sendMessage(). We wrap sendMessage in a Promise and resolve using the response.

What is stored in Background local storage

There are 4 things stored in Background local storage: encrypted wallet, wallet key ID, application state, and url allow list. These are all currently stored in local storage in plain text, but the plan is to move them to Chrome's virtual storage (chrome.storage). @stellar/wallet-sdk needs to be updated to support this.

The wallet is created during onboarding when the UI calls loadAccount. It saves the generated wallet in local storage along with the key id.

Application state is what we use to determine where the user is in the onboarding process. It is updated as we complete certain tasks, such as creating a new wallet or confirming a mnemonic phrase.

The allow list is an array of url's the user has confirmed are safe. If a url makes a request for a public key and it is on the allow list, Background will simply send the data back. Otherwise, it will open a new window with a prompt asking the user if the website is okay to trust.

What is stored in Background Redux store

Because Background runs for as long as Chrome is open, we can store some sensitive data in memory so we have access to it without it ever being available in plain text. We do this by saving public key, mnemonic phrase, and private key in a separate Redux store for Background.

As soon as a user creates/recovers an account, their public key will be added to the current state. The same is true of mnemonic phrase. When the UI requests either of them, we retrieve them from the Redux store and transmit them.

Private key is also stored in Redux. This is useful for signing transactions. However, this is different than public keys and mnemonic phrases in that we require a user to enter their password to retrieve the private key from the wallet and store it in Redux. Upon password enter, we also start a timer that removes the private key from the store after a certain amount of time, thus creating a small window in which Background has access to the private key. Also, we never transmit the private key to the UI, instead opting to transmit a flag that simply tells the UI if Background currently have the private key or not.

Popup

The Popup is the UI with which the end user will be interacting, separating all UI concerns into one folder. This is structured as a traditional React/Redux app, hydrating the store from the Background. Any view the end user interacts with (from onboarding, to account status, to interstitial screens to confirm a website's access to Lyra data) are all stored here. This allows for ease of sharing styles and Redux store. On page load, we call loadAccount to get the current data about the user from Background and hydrate the store.

What is stored in Popup Redux store

We only store values that are needed across different screens in Redux. This includes public key, application state, any errors Background has sent us, and a boolean telling us if Background currently has access to the private key (though not the private key itself).

Routing

We use react-router to show appropriate views while maintaining one store throughout. Several routes serve multiple purposes, redirecting to certain UI's based on what state the user is in. For example, if a user is new, the / route will lead to a screen that lets the user start a new account or retrieve an old account. But, if that user has confirmed their mnemonic phrase and we have their public key, we show them their account details. This is done by checking to see what keys we have stored in memory and what their application state is. Application state is generally more valuable in knowing where a user left off in onboarding while checking for the existence of public/private keys is more valuable in knowing if a user needs to authenticate.

If the user is navigated to a screen that requires a public key or the private key, we redirect them to /unlock-account to authenticate and retrieve the keys from Background before they are passed on to their ultimate destination.

Content Script

Content Script is what the extension loads on a client website when a user has Lyra installed. This is the second user facing component of the extension. This is the gateway for a client website to interact with Lyra. Content Script shares the same window as a client website. So a client site, like laboratory.stellar.org for example, can use window.postMessage to directly communicate with Content Script by post a messge to its own window. This is valuable because although a client site can't communicate directly with Background, Content Script can. So, a client site will tag a message as being for Lyra, and then post it to itself. Content Script will listen to that message, determine that it is intended for Lyra, and then pass on to Background. Background will check the requesting site against the allow list and then either a) determine everything checks out and send the data back to Content Script (which will in turn pass on to the client) or b) open a new window asking the end user to confirm that they want to allow the website to complete the action before Background moves on to scenario a.

The plan is to have this window.postMessage system wrapped in a separate npm module that a client website can install and implement. They should be able to call simple methods like const publicKey = await lyraAPI.getPublicKey(); to get the data they want.