Setup up a CRYPTO Balance widget on IOS with Node and scriptable
chowderhead
Posted on May 6, 2021
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:
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.
Now use this URL to clone it to your computer with any method you think is best.
now lets initialize the repo with npm: defaults should be fine
cd /scriptable-crypto-balance-widget
npm init
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"
}
...
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"
},
...
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"
}
}
now that we have that all laced up, lets install our dependencies:
npm install
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)
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
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'
make sure your logged in before you run this next command:
heroku create crypto-balance-widget
git push heroku main
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) {
}
})
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) {
}
})
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)
...
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)
}
})
...
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)
}
})
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)
}
}
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)
}
}
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)
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()
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
}
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
}
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 }
}
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 }
}
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.
Posted on May 6, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024