NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Blockcard TERN Price
// @namespace https://openuserjs.org/user/mmstick
// @description Show the value of TERN in the dashboard
// @version 0.4.1
// @author Michael Aaron Murphy <mmstick@pm.me>
// @match https://dashboard.getblockcard.com/*
// @grant none
// @run-at document-idle
// @copyright 2020, Michael Aaron Murphy
// @license MIT
// ==/UserScript==
/** TODO:
* Not yet experienced enough in frontend web dev to know how to asynchronously
* reload the script whenever the URL changes, since the Blockcard dashboard is
* a single-page web app, and navigating across pages breaks the script. Any
* help would be appreciated here.
*/
const DASHBOARD = "https://dashboard.getblockcard.com/dashboard"
/**
* @param {string} currentLocation
* @param {number} currentPrice
* @param {boolean} is_running
* @param {string} lastUsdBalance
* @param {null | HTMLElement} ternPriceElement
* @param {number} usdBalance
*/
class App {
constructor() {
this.currentLocation = `${window.location}`
this.currentPrice = 0
this.is_running = false
this.lastUsdBalance = ""
this.ternPriceElement = null
this.usdBalance = 0
}
/**
* Injects a custom Tern Price and Tern Price Alert card
*/
injectTernPriceCard() {
const baseAssetBlocks = document.getElementsByClassName("base-asset-block")
if (!baseAssetBlocks || baseAssetBlocks.length < 1) return false;
let dataAttributes = "";
for (const attr of baseAssetBlocks[0].attributes) {
if (attr.name.startsWith("data-v")) {
dataAttributes += " " + attr.name
}
}
const TERN_PRICE_ID = "tern-price-here"
let html = `<div id="tern-price" class="divider pl-8 col-md-4 col-12">
<p class="balance-type">TERN Price</p>
<p ${dataAttributes} id="${TERN_PRICE_ID}" class="balance" onclick="priceButton()">0</p>
</div>`;
baseAssetBlocks[0].insertAdjacentHTML("beforebegin", html)
this.ternPriceElement = document.getElementById(TERN_PRICE_ID)
// Now add the price alert element
html = `<div id="price-alert" class="divider pl-8 col-md-4 col-12">
<p class="balance-type">TERN Price Alert</p>
<form style="display:flex; flex-direction:row" onsubmit="return false">
<div><b style="font-size:1.35rem">$</b></div>
<input type="text" name="price-above" id="price-above" />
</form>
<style>
#price-above {
border: .2rem solid #333;
font-weight: 600;
margin-left: .5rem
}
</style>
</div>`;
baseAssetBlocks[0].insertAdjacentHTML("beforebegin", html)
return true
}
/**
* Fetch the USD and TERN balance cards from the page
*
* @returns {[HTMLElement, HTMLElement] | null}
*/
fetchBalances() {
const balances = document.getElementsByClassName("balance");
if (!balances || balances.length < 2) return null
const [, u, , t] = balances;
return [u, t]
}
/**
* Check the price-above value and show a notification if the current price is above it.
*/
priceAlertCheck() {
const alert = document.getElementById("price-above");
if (alert) {
const value = parseFloat(alert.value)
if (!isNaN(value)) {
if (value < this.currentPrice) {
this.priceAlertShow()
}
}
}
}
/**
* Show the price alert desktop notification
*/
priceAlertShow() {
const showNotification = () => {
if (document.visibilityState === "visible") {
return
}
const title = `TERN Value is $${this.currentPrice}`
const body = `Your balance is $${this.usdBalance.toFixed(2)}`
const notification = new Notification(title, {
body
})
notification.onclick = () => {
window.parent.focus()
notification.close()
}
}
if (Notification.permission === "granted") showNotification()
}
/**
* Adds the price of TERN in the TERN Balance card
*
* @param {string} usdb The USD balance
* @param {string} ternb The TERN balance
* @returns {string | null}
*/
ternValueCalculate(usdb, ternb) {
if (this.lastUsdBalance === usdb) return null;
this.lastUsdBalance = usdb
const ntern = ternb.split(' ')[0].replace(/,/g, '');
const tern = parseFloat(ntern)
this.usdBalance = parseFloat(usdb.split(' ')[1].replace(/,/g, ''))
this.currentPrice = (this.usdBalance / tern).toFixed(5)
return "$" + this.currentPrice;
}
/**
* Fetch the balance elements, parse them, and add the TERN value
*/
ternValueCheck() {
const balances = this.fetchBalances()
if (!balances) return false;
const [usdb, ternb] = balances;
this.ternValueUpdate(usdb, ternb)
return true
}
/**
* Calculate the TERN value and apply it to the TERN Balance card
*
* @param {HTMLElement} usdb
* @param {HTMLElement} ternb
*/
ternValueUpdate(usdb, ternb) {
const calculated = this.ternValueCalculate(usdb.innerText, ternb.innerText)
if (calculated) this.ternPriceElement.innerText = calculated
}
/**
* Start watching the price of Tern
*/
async start() {
app.is_running = true
await doUntil(1000, () => {
return !this.injectTernPriceCard()
})
this.ternValueCheck()
await doUntil(15000, () => {
this.ternValueCheck()
this.priceAlertCheck()
return true
})
app.is_running = false
}
}
/**
* Asynchronously sleep for the defined milliseconds before advancing
*
* @param {number} ms
*/
async function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
/**
* Repeatedly call a closure at regular intervals until it returns `false`
*
* @param {number} ms
* @param {() => boolean} func
*/
async function doUntil(ms, func) {
let cont = true
while (cont) {
await sleep(ms)
cont = func()
}
}
Notification.requestPermission()
const app = new App()
if (DASHBOARD === app.currentLocation) app.start()