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()