DEV Community

loading...
Cover image for Setup up a CRYPTO Balance widget on IOS with Node and scriptable

Setup up a CRYPTO Balance widget on IOS with Node and scriptable

chowderhead
code is my paint and the interwebs is my canvas. [https://kenchambers.dev]
・8 min read

About me: https://kenchambers.dev

I've been having so much fun with Scriptable which allows you to create IOS widgets using javascript code! I've also been delving into the crypto world for funsies, and I need a way to track my wallet addresses easily on my phone so i can obsess and check them all the time.

I plan on expanding this tutorial, depending on the feedback i get , I had to take alot of things out for the purposes of this tutorial since i needed a custom build for Coinmetro and blockfi, since their API interactions where a little more complicated.

If the feedback is good on this article, i'll open up my code for the chart as well!

Please note that your widget by the end of this will look like this:

lesser

Enjoy!


references:

https://devcenter.heroku.com/articles/getting-started-with-nodejs
https://devcenter.heroku.com/articles/deploying-nodejs
https://dev.to/matthri/create-your-own-ios-widget-with-javascript-5a11

Code:

https://github.com/nodefiend/scriptable-crypto-balance-widget

assumptions:
  • Node.js and npm installed.

- you have heroku CLI and you are logged in, if not click here

Setting up your repo:

To make things super easy, lets create a new repo on Github and clone it to our computer.

setup_github

Now use this URL to clone it to your computer with any method you think is best.

clonewars

now lets initialize the repo with npm: defaults should be fine

cd /scriptable-crypto-balance-widget
npm init
Enter fullscreen mode Exit fullscreen mode

add this to package json, so we can specify version of node, and add the dependencies we will need:

package.json

...
    "engines": {
    "node": "14.2.0"
  },
  "dependencies": {
    "axios": "^0.21.1",
    "express": "^4.17.1"
  }
...
Enter fullscreen mode Exit fullscreen mode

we need to specify what happens when npm start is run: (so also add this to package.json)

package.json

...
"scripts": {
    "start": "node index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
 ...
Enter fullscreen mode Exit fullscreen mode

Heres my final package JSON file:

package.json

{
  "name": "scriptable-crypto-balance-widget",
  "version": "1.0.0",
  "description": "A scriptable widget for checking crypto wallet balance",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/nodefiend/scriptable-crypto-balance-widget.git"
  },
  "author": "",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/nodefiend/scriptable-crypto-balance-widget/issues"
  },
  "homepage": "https://github.com/nodefiend/scriptable-crypto-balance-widget#readme",
  "engines": {
    "node": "14.2.0"
  },
  "dependencies": {
    "axios": "^0.21.1",
    "express": "^4.17.1"
  }
}

Enter fullscreen mode Exit fullscreen mode

now that we have that all laced up, lets install our dependencies:

npm install

Enter fullscreen mode Exit fullscreen mode

Now lets build our node server:

index.js

const axios = require('axios')
const express = require('express')
const app = express()
const port = 5000

app.listen(process.env.PORT || port)

Enter fullscreen mode Exit fullscreen mode

You should have a pre-generated .gitignore, but make sure it has at least these things, to prevent build artifacts from being deployed to heroku:

.gitignore

/node_modules
npm-debug.log
.DS_Store
/*.env

Enter fullscreen mode Exit fullscreen mode

sweet, we should have 3 files in our git commit history:

index.js
package.json
package-lock.json

Deploy to heroku

git add .
git commit -m 'first commit'

Enter fullscreen mode Exit fullscreen mode

make sure your logged in before you run this next command:

heroku create crypto-balance-widget
git push heroku main
Enter fullscreen mode Exit fullscreen mode

This will automatically deploy to heroku, and push to the main branch.

It will give you a public URL to hit our new heroku server, but we don't have anything up there yet so lets just add some code before we make any requests to it.

Creating a route to return BTC Price

So for brevity, I have all this code in the same index.js file, but I would reccomend DRYing it up, or sticking it in a class or at least a separate file.

Lets start by creating our first route, a simple GET /balance endpoint in which our widget will be sending a request to:

app.get('/balance', async function (req, res) {
  try {

  } catch (err) {


  }
})

Enter fullscreen mode Exit fullscreen mode

inside of our try catch, we want to run two async requests that we have not written yet, these functions will gather the price of the BTC , and the amount inside of the crypto wallet.

Please note, if you wanted to get wallet prices of a different crypto, you would simply change the contents of these functions, to hit different APIS for different crypto networks.

app.get('/balance', async function (req, res) {
  try {
        let [ walletBalance, btcPrice ] = await Promise.all([
      getWalletBalance(), getBTCPrice()
    ])

  } catch (err) {


  }
})

Enter fullscreen mode Exit fullscreen mode

Now that we have the price and amount in the wallet, we simply multiply these together and send back a response to our request:

...
    let balance = (walletBalance * btcPrice).toFixed(2)

    const response = {
      statusCode: 200,
      body: balance
    }
    res.send(response)

...
Enter fullscreen mode Exit fullscreen mode

And if there is an error, lets catch it and also return a response:

...
  } catch (err) {
    const response = {
      statusCode: 500,
      body: err
    }
    res.send(response)
  }
})
...

Enter fullscreen mode Exit fullscreen mode

Heres what our request looks like in completion:

app.get('/balance', async function (req, res) {
  try {
    let [ walletBalance, btcPrice ] = await Promise.all([
      getBTCWallet(), getBTCPrice()
    ])

    let balance = (walletBalance * btcPrice).toFixed(2)

    const response = {
      statusCode: 200,
      body: balance
    }
    res.send(response)
  } catch (err) {
    const response = {
      statusCode: 500,
      body: err
    }
    res.send(response)
  }
})


Enter fullscreen mode Exit fullscreen mode

Alright, lets write getWalletBalance() and getBTCPrice() so that we can use them in the above function:

This async function will hit testnet-api to retrieve the current price of bitcoin. If you know a different API you could replace the URL here, just make sure to update the parsing of the response, since the JSON data will be shaped differently.

async function getBTCPrice() {
  try {
    let response = await axios({
      method: 'get',
      url: 'https://testnet-api.smartbit.com.au/v1/exchange-rates'
    })
    let price = response.data['exchange_rates'].filter(function(rate){ return rate['code'] == 'USD'})
    return price[0]['rate']
  } catch (e) {
    console.log(e)
  }
}
Enter fullscreen mode Exit fullscreen mode

Next we will write our function to retrieve the balance of a existing crypto wallet. The same applies for this function, we can update the API we are using simply by switching out smartbitURL or we can update the wallet address simply by switching out the wallet variable. If you do switch out the API, make sure to update the response, as it will most likely be shaped differently.

Since the wallet balance came back as a string, I turned it into a number so we can easily multiply it by the current price of bitcoin.


async function getBTCWallet(){
  let wallet = '3P3QsMVK89JBNqZQv5zMAKG8FK3kJM4rjt'
  let smartbitURL = 'https://api.smartbit.com.au/v1/blockchain/address/' + wallet

  try {
    let response = await axios({
      method: 'get',
      url: smartbitURL
    })

    let walletBalance = parseFloat(response.data['address']['total']['balance'])
    return walletBalance
  } catch (e) {
       console.log(e)
  }
}
Enter fullscreen mode Exit fullscreen mode

All together now, our index.js should look like this

const axios = require('axios')
const express = require('express')
const app = express()
const port = 5000

async function getBTCPrice() {
  try {
    let response = await axios({
      method: 'get',
      url: 'https://testnet-api.smartbit.com.au/v1/exchange-rates'
    })
    let price = response.data['exchange_rates'].filter(function(rate){ return rate['code'] == 'USD'})
    return price[0]['rate']
  } catch (e) {
    console.log(e)
  }
}

async function getBTCWallet(){
  let wallet = '3P3QsMVK89JBNqZQv5zMAKG8FK3kJM4rjt'
  let smartbitURL = 'https://api.smartbit.com.au/v1/blockchain/address/' + wallet

  try {
    let response = await axios({
      method: 'get',
      url: smartbitURL
    })

    let walletBalance = parseFloat(response.data['address']['total']['balance'])
    return walletBalance
  } catch (e) {
       console.log(e)
  }
}

app.get('/balance', async function (req, res) {
  try {
    let [ walletBalance, btcPrice ] = await Promise.all([
      getBTCWallet(), getBTCPrice()
    ])

    let balance = (walletBalance * btcPrice).toFixed(2)

    const response = {
      statusCode: 200,
      body: balance
    }
    res.send(response)
  } catch (err) {
    const response = {
      statusCode: 500,
      body: err
    }
    res.send(response)
  }
})

console.log("App is running on ", port);

app.listen(process.env.PORT || port)


Enter fullscreen mode Exit fullscreen mode

Lets commit our changes now to heroku:

git heroku push main

now that our changes are up , we should be able to contact our server via our scriptable widget:

Scriptable Widget:

Scriptable is an app that we can download off the app store.

you can set up the app to run different scripts, since this article is more about the code aspect, I wont cover how to set up scriptable and run a script, you can decern that from this article here

This is a great article because it covers how to send async requests.

first lets write the function that will create the widget:

let widget = await createWidget()
if (config.runsInWidget) {
  Script.setWidget(widget)
} else {
  widget.presentMedium()
}
Script.complete()
Enter fullscreen mode Exit fullscreen mode

Now lets build the meat and potatoes, createWidget()

async function createWidget() {
  // declare widget     
  let w = new ListWidget()
  // call async request to fetch wallet amount
  let { balance } = await fetchBitcoinWalletAmount()
  //background color
  w.backgroundColor = new Color("#000000")
  // **************************************
  //header icon
  let docsSymbol = SFSymbol.named("bitcoinsign.square")
  let bitcoinIconImage = w.addImage(docsSymbol.image)
  bitcoinIconImage.rightAlignImage()
  bitcoinIconImage.imageSize = new Size(25, 25)
  bitcoinIconImage.tintColor = Color.green()
  bitcoinIconImage.imageOpacity = 0.8
  bitcoinIconImage.url = "https://www.google.com"
  // **************************************
  // MAIN CONTAINER
  let mainContainerStack = w.addStack()
  // TOP CONTAINER
  let leftContainerStack = mainContainerStack.addStack()
  leftContainerStack.layoutVertically()
  let rightContainerStack = mainContainerStack.addStack()
  rightContainerStack.layoutVertically()
  // TOP LEFT STACK:
  // **************************************
  // Large Bal
  let largeFont = Font.largeTitle(20)
  const largeBalanceStack = leftContainerStack.addStack()
  const largeBalance = largeBalanceStack.addText('$' + (balance).toString())
  largeBalance.font = largeFont
  largeBalance.textColor = new Color('#ffffff')

  // **************************************
  //refresh widget automatically
  let nextRefresh = Date.now() + 1000
  w.refreshAfterDate = new Date(nextRefresh)
  showGradientBackground(w)
  return w
}

Enter fullscreen mode Exit fullscreen mode

lets write our function for applying a gradient background.


function showGradientBackground(widget) {
  let gradient = new LinearGradient()
  gradient.colors = [new Color("#0a0a0a"), new Color("#141414"), new Color("#1f1f1f")]
  gradient.locations = [0,0.8,1]
  widget.backgroundGradient = gradient
}
Enter fullscreen mode Exit fullscreen mode

Now that we have the widget set up, lets build our fetchBitcoinWalletAmount() function.
This will be composed of two async functions, of course you can format it a number of different ways depending on your code style, but because this is a watered down version of my actual widget, its broken into two functions.

async function getBalance(){
  let BTCUrl = 'http://localhost:5000/balance'
  let request = new Request(BTCUrl)
  request.method = "get";
  let response = await request.loadJSON()
  return response.body
}
// fetch bitcoin wallet amount
async function fetchBitcoinWalletAmount(){
  let btcBalanceAmount = await getBalance()
  return { balance: btcBalanceAmount }
}
Enter fullscreen mode Exit fullscreen mode

Now all together, here is our scriptable.js file- it can be found in the code repo as well.

A good way to trouble shoot this function , and if you want to code on your computer instead of your phone, use this download:

https://scriptable.app/mac-beta/

scriptable.js

// ************************************
// execute widget
let widget = await createWidget()
if (config.runsInWidget) {
  Script.setWidget(widget)
} else {
  widget.presentMedium()
}
Script.complete()
// ************************************
async function createWidget() {
  // declare widget     
  let w = new ListWidget()
  // call async request to fetch wallet amount
  let { balance } = await fetchBitcoinWalletAmount()
  //background color
  w.backgroundColor = new Color("#000000")
  // **************************************
  //header icon
  let docsSymbol = SFSymbol.named("bitcoinsign.square")
  let bitcoinIconImage = w.addImage(docsSymbol.image)
  bitcoinIconImage.rightAlignImage()
  bitcoinIconImage.imageSize = new Size(25, 25)
  bitcoinIconImage.tintColor = Color.green()
  bitcoinIconImage.imageOpacity = 0.8
  bitcoinIconImage.url = "https://www.google.com"
  // **************************************
  // MAIN CONTAINER
  let mainContainerStack = w.addStack()
  // TOP CONTAINER
  let leftContainerStack = mainContainerStack.addStack()
  leftContainerStack.layoutVertically()
  let rightContainerStack = mainContainerStack.addStack()
  rightContainerStack.layoutVertically()
  // TOP LEFT STACK:
  // **************************************
  // Large Bal
  let largeFont = Font.largeTitle(20)
  const largeBalanceStack = leftContainerStack.addStack()
  const largeBalance = largeBalanceStack.addText('$' + (balance).toString())
  largeBalance.font = largeFont
  largeBalance.textColor = new Color('#ffffff')

  // **************************************
  //refresh widget automatically
  let nextRefresh = Date.now() + 1000
  w.refreshAfterDate = new Date(nextRefresh)
  // add gradient to widget
  showGradientBackground(w)
  return w
}
function showGradientBackground(widget) {
  let gradient = new LinearGradient()
  gradient.colors = [new Color("#0a0a0a"), new Color("#141414"), new Color("#1f1f1f")]
  gradient.locations = [0,0.8,1]
  widget.backgroundGradient = gradient
}

async function getBalance(){
  let BTCUrl = 'http://localhost:5000/balance'
  let request = new Request(BTCUrl)
  request.method = "get";
  let response = await request.loadJSON()
  return response.body
}
// fetch bitcoin wallet amount
async function fetchBitcoinWalletAmount(){
  let btcBalanceAmount = await getBalance()
  return { balance: btcBalanceAmount }
}

Enter fullscreen mode Exit fullscreen mode

and Voaila! we have our crypto balance in an IOS app.

to push your code to heroku, use :

git push heroku [branch]

and then get the URL of your app from the heroku dashboard and plug it into our scriptable.js file on the widget in place of: localhost:5000

I plan to write more on to include a graph that will display something, maybe a history of your balances? or maybe the current price of crypto? let me know down below in the comments.

This is kind of a huge tutorial, so if you have any issues with it, leave me a message in the comments.

Or if you want to fight me, cause my code is so deplorable- please let me know as well.

Discussion (0)

Forem Open with the Forem app