Cryptocurrencies

Developing a cryptocurrency price monitor using Firebase and Google Cloud Platform

In this article I demonstrate how to build a cryptocurrency price monitoring app using Firebase and Google Cloud Platform (GCP). The app receives notifications whenever the price of Bitcoin or Ethereum change in the market. Users can configure minimum and maximum price thresholds for each cryptocurrency, and we use these settings to determine which users to notify for any given price change.

The inspiration for this app came from an article published by the Pusher community (read part 1 and part 2 here). Their cryptocurrency price alerts app uses Pusher for delivering notifications, which seems to be using Firebase Cloud Messaging (FCM) under the hood. Furthermore they employ a SQLite database and a standalone back-end server developed in Go to implement the necessary functionality. As I was reading that article I couldn’t help but think that the use case in question would make a great demo for Firebase and GCP as well. This article and the associated app are results of that notion.

The version of the app we develop does not require deploying a standalone database or a back-end server. We implement all the server-side functionality using various Platform-as-a-Service (PaaS) and serverless products readily available in the Firebase and GCP ecosystems. Specifically, our example app demonstrates the following features.

  • Storing cryptocurrency prices and per-user threshold settings in Google Cloud Firestore.
  • Periodically checking the cryptocurrency market prices using a Google App Engine service.
  • Using Google Cloud Functions to determine which users to notify about each price change.
  • Sending push notifications via FCM.

In the client-side we use the Firebase Android SDK to directly interact with Firestore and FCM. This precludes the need for implementing a separate REST API, or having to program any HTTP interactions in the Android app.

Firestore

Our app uses two Firestore collections.

  • The prices collection is used to store the latest cryptocurrency prices. It will only have two documents — one for each currency that we wish to monitor. We shall assign them the IDs btc and eth respectively.
  • The prefs collection is where we store per-user settings. Each app instance creates a document in this collection with its unique Firebase instance ID as the document identifier.

There’s no need to manually create these collections. They get created automatically as our app goes into action. But if you wish to test the app by adding some sample data, figure 1 shows what your Firestore database should look like.

Figure 1: Firestore collections used by the app

As part of setting up Firestore, we deploy the following security rules. These essentially make the prices collection read-only, and the prefs collection read-write. Any other collections you happen to have in the same Firestore database will be inaccessible to the app.

service cloud.firestore {
match /databases/{database}/documents {
match /prices/{currency} {
allow read;
allow write: if false;
}
match /prefs/{device} {
allow read, write;
}
}
}

We don’t implement user authentication in this demo, but it is fairly straightforward to do so using Firebase Auth. If you decide to explore that option, you can use the unique user IDs provided by Firebase Auth as the document IDs in the prefs collection. That would also allow you to further tighten up your security rules, allowing each user to write only to their designated documents.

Also note that Firebase security rules only apply to the client apps (Android users in this case). In the back-end we use Firebase Admin SDKs, which bypass security rules, and access the database as a privileged user.

The Android Client App

The full source of the app can be found in my firecloud GitHub repo. The whole thing amounts to about 170 lines of Kotlin code, plus the usual Android resources and manifest files. We start a Firestore realtime listener on the prices collection when the app launches. This way the app always displays the latest cryptocurrency prices stored in Firestore. Listing 1 shows the relevant code fragment from the MainActivity.

Listing 1: Receiving realtime updates using the Firestore Android SDK

The MainActivity layout contains two text views with the IDs btc and eth. Note that we use the same IDs for Firestore documents in the prices collection. Therefore we can employ the trick in lines 16–18 to map each Firestore document to a text view in the UI.

Tapping on a text view launches the settings dialog for the corresponding cryptocurrency. The user can specify a minimum and a maximum threshold and save the settings. The idea is that the app will notify the user whenever the price drops below the min threshold or exceeds the max threshold. Listing 2 shows the method that saves the settings to Firestore.

Listing 2: Saving user preferences to Firestore

In addition to the threshold values, we also save app instance’s registration token to Firestore (line 6). This is used later when we want to notify the device via FCM. We use the stable Firebase instance ID as the document identifier. In an app that implements user authentication, we can use the unique user ID here instead.

The await() method used in listing 3 (lines 2 and 9) is an extension method we have added to the Android GMS Core’s Task class. This makes it easier to use the Task API with Kotlin coroutines. Listing 3 shows the implementation of the await() method.

Listing 3: Suspendable extension method added to the Android Task API

Finally, we add the FCM Android SDK to the app, and extend the FirebaseMessagingService as shown in listing 4 in order to receive push notifications.

Listing 4: Receiving FCM push notifications in Android

This service handles incoming push notifications when the app is in foreground. It displays a simple pop-up with the notification payload. When the app is in the background the notification will be delivered to the Android’s system notification tray.

That’s pretty much all the exciting bits in the client application. Now lets look at the back-end components of the app.

Cryptocurrency price checker

We implement an App Engine service in Go (v1.11) to periodically check the cryptocurrency market prices, and save the results to Firestore. Our implementation is comprised of following files:

cryptocron/
├── cryptocron.go (core functionality of the service)
└── web/
├── app.yaml (App Engine deployment descriptor)
├── cron.yaml (App Engine cron configuration)
└── main.go (main function that exposes the service over HTTP)

Full source code of the service can be found in the firecloud repo. You can run the following command to directly import the code into your GOPATH:

$ go get github.com/hiranya911/firecloud/crypto-fire-alert/cryptocron

The price checker service uses the Firebase Admin SDK for Go to access Firestore. Since our code is going to be deployed in App Engine, we can let the SDK auto discover Google Application Default Credentials (ADC) to authorize Firestore API calls. This means we can initialize the Admin SDK with a minimal configuration as shown in listing 5.

Listing 5: Initializing the Admin Go SDK for Firestore access

Note that we are only passing a Context to the firebase.NewApp() function. The SDK is able to auto discover authorization credentials and any other settings required to access Firestore (e.g. GCP project ID). The firestore.Client that we subsequently obtain at line 19 provides access to the same Firestore database used by the Android client apps.

We use the CryptoCompare REST API to fetch the latest prices of Bitcoin and Ethereum. Listing 6 shows how the discovered prices are saved to Firestore.

Listing 6: Saving cryptocurrency prices to Firestore from Go

We execute a batched write on Firestore to update the prices of both Bitcoin and Ethereum in a single operation. Since our Android app is already listening to updates in the prices collection, these changes become immediately visible to the users.

We expose this service as an HTTP endpoint at the URL path /fetch. Next, in order to keep our app data up-to-date, we need to instruct Google App Engine to invoke our service periodically. This is done by writing a cron.yaml file as shown in listing 7.

Listing 7: App Engine cron job configuration

To test this service locally, set the GOOGLE_APPLICATION_CREDENTIALS environment variable to point to a service account JSON file downloaded from your Firebase project. Then execute the main.go file of the service:

$ export GOOGLE_APPLICATION_CREDENTIALS=path/to/serviceAccount.json
$ go run main.go

This starts the service on port 8080, and you can try it out by sending a request to http://localhost:8080/fetch. You will see the prices collection getting updated in the Firebase console as a result. The updates will also appear on the Android client app, if it happens to be running at the time. Note that when testing locally, you must manually invoke the /fetch endpoint of the service. The cron.yaml file only takes effect once deployed to the cloud.

The rate at which cryptocurrency prices change in the real world may not be enough to trigger many updates while testing the service. If this becomes a problem you may forego the CryptoCompare API, and get the service to produce random pricing data at each invocation. Set the following environment variable prior to launching the service to enable this feature.

$ export SIMULATE_MODE=1

To deploy the service to App Engine, install and set up the Google Cloud SDK. Make sure the gcloud command-line utility is configured to manage your GCP/Firebase project.

$ gcloud config set project <your-project-id>

Then run the following commands from the same directory as the app.yaml file.

$ gcloud app deploy
$ gcloud app deploy cron.yaml

The first command deploys the service implementation. The latter starts the scheduled task that periodically invokes the service. Shortly afterwards, you will see entries similar to the following in your App Engine log.

2018-12-26 13:47:19.926 PST GET 200 203 B 688 ms AppEngine-Google; /fetch
2018-12-26 13:47:20.613 PST 2018/12/26 21:47:20 Price of btc = 3820.57 USD
2018-12-26 13:47:20.613 PST 2018/12/26 21:47:20 Price of eth = 130.76 USD

We have also programmed our service to accept requests only from the App Engine cron scheduler. Trying to manually invoke the HTTP endpoint in the cloud will yield a 404 Not Found response. You can change this behavior by removing the CRON_ONLY environment variable from the app.yaml file.

Notification sender

The last piece we need to complete our app is the service that notifies interested users when the cryptocurrency prices change. We already have a service that updates the prices in Firestore. Therefore we can use Cloud Functions for Firebase to implement a serverless function that sends out notifications whenever a new price is written to the prices collection of Firestore. Listing 8 illustrates what this implementation looks like.

Listing 8: Sending targeted notifications with Cloud Functions for Firebase

We define an onUpdate Firestore trigger for the documents in the prices collection. The ID of the document that is being updated (i.e btc or eth), and its price value can be obtained from the arguments passed into the trigger. We pass these values to the findTargetDevices() helper method, which queries the prefs collection in Firestore to determine the users that should be notified of the price change.

We wish to notify users whose min threshold is higher than the current price, or whose max threshold is lower than it. Since Firestore does not support disjunctive queries (i.e. OR queries), we run two separate queries (lines 33–34), and aggregate the results. This is also where we reference the device registration tokens that the Android clients have saved in Firestore. Finally we use the FCM API in the Admin SDK to send push notifications to the selected users.

Again, notice that we are initializing the Firebase Admin SDK with a minimal configuration (lines 4–5). The SDK auto discovers valid authorization credentials, and connects to the same Firestore database used by Android clients and the App Engine service.

The full implementation of the cloud function is available on GitHub. Use the Firebase functions emulator to test the code locally. Run the following commands from the functions/ directory of the project.

$ export GOOGLE_APPLICATION_CREDENTIALS=path/to/serviceAccount.json
$ npm run shell

This launches the Firebase emulator shell. You can now directly invoke the function with some sample data as shown below.

firebase > sendCryptoAlerts({before: {}, after: {value: 5000, name: 'BTC'}}, {params: {currency: 'btc'}})
'Successfully invoked function.'
info: User function triggered, starting execution
info: Price of btc changed to USD 5000
info: Execution took 2263 ms, user function completed successfully

Having verified our function works as expected, we can deploy it to the cloud using the Firebase CLI.

$ firebase deploy --only functions

You can now either wait for the App Engine service to update the cryptocurrency prices, or enter some sample prices into Firestore manually. It is also possible to manually run the App Engine cron job using the GCP console. Either way, the cloud function will get triggered, and you will be able to see the corresponding logs in the Firebase console.

1:47:21.821 PM sendCryptoAlerts Function execution took 145 ms, finished with status: 'ok'
1:47:21.740 PM sendCryptoAlerts Notifying 1 devices
1:47:21.679 PM sendCryptoAlerts Price of eth changed to USD 130.76
1:47:21.677 PM sendCryptoAlerts Function execution starte
1:47:21.673 PM sendCryptoAlerts Function execution took 516 ms, finished with status: 'ok'
1:47:21.444 PM sendCryptoAlerts Notifying 1 devices
1:47:21.258 PM sendCryptoAlerts Price of btc changed to USD 3820.57
1:47:21.158 PM sendCryptoAlerts Function execution started

Figure 2 is a screenshot from the GCP console, showing both App Engine and Cloud Functions logs in the same window. All the entries shown in this figure were produced by a single run of the App Engine service.

Figure 2: App Engine and Cloud Functions logs produced by a price change

Figure 3 shows various screens of the Android client app, including how a notification is being delivered while the app is in the background.

Figure 3: Android client app UI

Conclusion

In this post we looked at how to develop a cryptocurrency price monitoring app using several Firebase and GCP products. We used the Firebase Android SDK to directly interact with Google Cloud Firestore and FCM. We implemented a service in Google App Engine to periodically check the market prices of Bitcoin and Ethereum. Finally, we implemented a Google Cloud Function that notifies users of price changes based on the price thresholds configured by individual users. The whole exercise took me a couple of hours, and you can find the full implementation on GitHub.

The back-end functionality of this app is split between Google App Engine and Google Cloud Functions. However, it is also possible to implement all the back-end functionality using App Engine alone. We can program our Go service to send push notifications every time it updates the cryptocurrency prices. In fact, the service I have implemented already supports this, but it is disabled by default. See if you can figure out how to enable push notifications in the App Engine service by going through the code.

Personally, I very much prefer the idea of having distinct services for checking the cryptocurrency prices, and sending push notifications. This makes separation of concerns more explicit while resulting in a more loosely coupled implementation. It also makes each service independently testable and deployable, thus bringing us closer to a microservices architecture. For instance, imagine trying to change how our app checks cryptocurrency prices. With two distinct services in place, we can simply update the price checking service, and never touch the notification sender.

I hope you find this article and the associated demo app useful. Please feel encouraged to reach out with any questions or feedback. If you have any demo app ideas that you would like me to try and implement, I would like to hear them too. As always, you are also welcome to engage with the Firebase community via various opensource Firebase repositories.

Source

neallesh@yahoo.co.uk

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.