@rahlfantasy/block-explorer
Version:
QRL block explorer
445 lines (403 loc) • 13.3 kB
JavaScript
/* eslint max-len: 0 */
import { HTTP } from 'meteor/http'
import { JsonRoutes } from 'meteor/simple:json-routes'
import sha512 from 'sha512'
// import helpers from '@theqrl/explorer-helpers'
/* eslint import/no-cycle: 0 */
import {
getLatestData, getObject, getStats, getPeersStat, apiCall, makeTxListHumanReadable,
} from '/imports/startup/server/index.js'
import {
Blocks, lasttx, homechart, quantausd, status, peerstats,
} from '/imports/api/index.js'
import { SHOR_PER_QUANTA } from '../both/index.js'
const refreshBlocks = () => {
const request = { filter: 'BLOCKHEADERS', offset: 0, quantity: 10 }
const response = Meteor.wrapAsync(getLatestData)(request)
// identify miner and calculate total transacted in block
response.blockheaders.forEach((value, key) => {
const req = {
query: Buffer.from(value.header.block_number.toString()),
}
const res = Meteor.wrapAsync(getObject)(req)
let totalTransacted = 0
res.block_extended.extended_transactions.forEach((val) => {
totalTransacted += parseInt(val.tx.fee, 10)
if (val.tx.transactionType === 'coinbase') {
response.blockheaders[key].minedBy = val.tx.coinbase.addr_to
totalTransacted += parseInt(val.tx.coinbase.amount, 10)
}
if (val.tx.transactionType === 'transfer') {
val.tx.transfer.amounts.forEach((xferAmount) => {
totalTransacted += parseInt(xferAmount, 10)
})
}
})
response.blockheaders[key].totalTransacted = totalTransacted
})
// Fetch current data
const current = Blocks.findOne()
// On vanilla build, current will be undefined so we can just insert data
if (current === undefined) {
Blocks.insert(response)
} else {
// Only update if data has changed.
let newData = false
_.each(response.blockheaders, (newBlock) => {
let thisFound = false
_.each(current.blockheaders, (currentBlock) => {
if (currentBlock.header.block_number === newBlock.header.block_number) {
thisFound = true
}
})
if (thisFound === false) {
newData = true
}
})
if (newData === true) {
// Clear and update cache as it's changed
Blocks.remove({})
Blocks.insert(response)
}
}
const lastblocktime = response.blockheaders[4].header.timestamp_seconds
const seconds = new Date().getTime() / 1000
const timeDiff = Math.floor((seconds - lastblocktime) / 60)
// needs refactor
if (timeDiff > 100000) {
const httpPostMessage = {
icon: 'https://vignette.wikia.nocookie.net/fantendo/images/3/3d/HC_Minion_Icon.png/revision/latest?cb=20140211171359',
title: 'Block Explorer Warning',
body: `**WARNING:** ${timeDiff} minutes since last block`,
}
// if there is a Glip webhook, post an alert to Glip
try {
if (Meteor.settings.glip.webhook.slice(0, 23) === 'https://hooks.glip.com/') {
const httpPostUrl = Meteor.settings.glip.webhook
try {
HTTP.call('POST', httpPostUrl, {
params: httpPostMessage,
})
return true
} catch (e) {
// Got a network error, timeout, or HTTP error in the 400 or 500 range.
}
}
} catch (er) {
// the glip variable isn't defined (i.e. block explorer started without a config file)
}
console.log(`**WARNING:** ${timeDiff} minutes since last block`) // eslint-disable-line
}
return true
}
function refreshLasttx() {
// First get confirmed transactions
const confirmed = Meteor.wrapAsync(getLatestData)({ filter: 'TRANSACTIONS', offset: 0, quantity: 10 })
// Now get unconfirmed transactions
const unconfirmed = Meteor.wrapAsync(getLatestData)({ filter: 'TRANSACTIONS_UNCONFIRMED', offset: 0, quantity: 10 })
// Merge the two together
const confirmedTxns = makeTxListHumanReadable(confirmed.transactions, true)
const unconfirmedTxns = makeTxListHumanReadable(unconfirmed.transactions_unconfirmed, false)
const merged = {}
merged.transactions = unconfirmedTxns.concat(confirmedTxns)
// Fetch current data
const current = lasttx.findOne()
// On vanilla build, current will be undefined so we can just insert data
if (current === undefined) {
lasttx.insert(merged)
} else {
// Only update if data has changed.
let newData = false
_.each(merged.transactions, (newTxn) => {
let thisFound = false
_.each(current.transactions, (currentTxn) => {
// Find a matching pair of transactions by transaction hash
if (currentTxn.tx.transaction_hash === newTxn.tx.transaction_hash) {
try {
// If they both have null header (unconfirmed) there is no change
if ((currentTxn.header === null) && (newTxn.header === null)) {
thisFound = true
// If they have same block number, there is also no change.
} else if (currentTxn.header.block_number === newTxn.header.block_number) {
thisFound = true
}
} catch (e) {
// Header in cached unconfirmed txn not found, we located a change
thisFound = false
}
}
})
if (thisFound === false) {
newData = true
}
})
if (newData === true) {
// Clear and update cache as it's changed
lasttx.remove({})
lasttx.insert(merged)
}
}
}
function refreshStats() {
const res = Meteor.wrapAsync(getStats)({ include_timeseries: true })
// Save status object
status.remove({})
status.insert(res)
// Start modifying data for home chart object
const chartLineData = {
labels: [],
datasets: [],
}
// Create chart axis objects
const labels = []
const hashPower = {
label: 'Hash Power (hps)',
borderColor: '#DC255D',
backgroundColor: '#DC255D',
fill: false,
data: [],
yAxisID: 'y-axis-2',
pointRadius: 0,
borderWidth: 2,
}
const difficulty = {
label: 'Difficulty',
borderColor: '#4A90E2',
backgroundColor: '#4A90E2',
fill: false,
data: [],
yAxisID: 'y-axis-2',
pointRadius: 0,
borderWidth: 2,
}
const movingAverage = {
label: 'Block Time Average (s)',
borderColor: '#20E7C9',
backgroundColor: '#20E7C9',
fill: false,
data: [],
yAxisID: 'y-axis-1',
pointRadius: 0,
borderWidth: 2,
}
const blockTime = {
label: 'Block Time (s)',
borderColor: '#444444',
backgroundColor: '#888888',
fill: false,
showLine: false,
data: [],
yAxisID: 'y-axis-1',
pointRadius: 2,
borderWidth: 2,
}
// Loop all API responses and push data into axis objects
_.each(res.block_timeseries, (entry) => {
labels.push(entry.number)
hashPower.data.push(entry.hash_power)
difficulty.data.push(entry.difficulty)
movingAverage.data.push(entry.time_movavg)
blockTime.data.push(entry.time_last)
})
// Push axis objects into chart data
chartLineData.labels = labels
chartLineData.datasets.push(hashPower)
chartLineData.datasets.push(difficulty)
chartLineData.datasets.push(movingAverage)
chartLineData.datasets.push(blockTime)
// Save in mongo
homechart.remove({})
homechart.insert(chartLineData)
}
const refreshQuantaUsd = () => {
const apiUrl = 'https://bittrex.com/api/v1.1/public/getmarketsummary?market=btc-qrl'
const apiUrlUSD = 'https://bittrex.com/api/v1.1/public/getmarketsummary?market=usdt-btc'
const response = Meteor.wrapAsync(apiCall)(apiUrl)
const responseUSD = Meteor.wrapAsync(apiCall)(apiUrlUSD)
const usd = response.result[0].Last * responseUSD.result[0].Last
const price = { price: usd }
quantausd.remove({})
quantausd.insert(price)
}
const refreshPeerStats = () => {
const response = Meteor.wrapAsync(getPeersStat)({})
// Convert bytes to string in response object
_.each(response.peers_stat, (peer, index) => {
response.peers_stat[index].peer_ip = sha512(Buffer.from(peer.peer_ip).toString()).toString('hex').slice(0, 10)
response.peers_stat[index].node_chain_state.header_hash = Buffer.from(peer.node_chain_state.header_hash).toString('hex')
response.peers_stat[index].node_chain_state.cumulative_difficulty = parseInt(Buffer.from(peer.node_chain_state.cumulative_difficulty).toString('hex'), 16)
})
// Update mongo collection
peerstats.remove({})
peerstats.insert(response)
}
// Refresh blocks every 20 seconds
Meteor.setInterval(() => {
refreshBlocks()
}, 20000)
// Refresh lasttx cache every 10 seconds
Meteor.setInterval(() => {
refreshLasttx()
}, 10000)
// Refresh Status / Home Chart Data 20 seconds
Meteor.setInterval(() => {
refreshStats()
}, 20000)
// Refresh Quanta/USD Value every 120 seconds
Meteor.setInterval(() => {
refreshQuantaUsd()
}, 120000)
// Refresh peer stats every 20 seconds
Meteor.setInterval(() => {
refreshPeerStats()
}, 20000)
// On first load - cache all elements.
Meteor.setTimeout(() => {
refreshBlocks()
refreshLasttx()
refreshStats()
refreshQuantaUsd()
refreshPeerStats()
}, 5000)
JsonRoutes.add('get', '/api/emission', (req, res) => {
let response = {}
const queryResults = status.findOne()
if (queryResults !== undefined) {
// cached transaction located
const emission = parseInt(queryResults.coins_emitted, 10) / SHOR_PER_QUANTA
response = { found: true, emission }
} else {
response = { found: false, message: 'API error', code: 5001 }
}
JsonRoutes.sendResult(res, {
data: response,
})
})
JsonRoutes.add('get', '/api/emission/text', (req, res) => {
let response = {}
const queryResults = status.findOne()
if (queryResults !== undefined) {
// cached transaction located
const emission = parseInt(queryResults.coins_emitted, 10) / SHOR_PER_QUANTA
response = emission
} else {
response = 'Error'
}
JsonRoutes.sendResult(res, {
data: response,
})
})
JsonRoutes.add('get', '/api/reward', (req, res) => {
let response = {}
const queryResults = status.findOne()
if (queryResults !== undefined) {
// cached transaction located
const reward = parseFloat(queryResults.block_last_reward) / SHOR_PER_QUANTA
response = { found: true, reward }
} else {
response = { found: false, message: 'API error', code: 5002 }
}
JsonRoutes.sendResult(res, {
data: response,
})
})
JsonRoutes.add('get', '/api/reward/text', (req, res) => {
let response = {}
const queryResults = status.findOne()
if (queryResults !== undefined) {
// cached transaction located
const reward = parseFloat(queryResults.block_last_reward) / SHOR_PER_QUANTA
response = reward
} else {
response = 'Error'
}
JsonRoutes.sendResult(res, {
data: response,
})
})
JsonRoutes.add('get', '/api/rewardshor', (req, res) => {
let response = {}
const queryResults = status.findOne()
if (queryResults !== undefined) {
// cached transaction located
const reward = parseFloat(queryResults.block_last_reward)
response = { found: true, reward }
} else {
response = { found: false, message: 'API error', code: 5002 }
}
JsonRoutes.sendResult(res, {
data: response,
})
})
JsonRoutes.add('get', '/api/rewardshor/text', (req, res) => {
let response = {}
const queryResults = status.findOne()
if (queryResults !== undefined) {
// cached transaction located
const reward = parseFloat(queryResults.block_last_reward)
response = reward
} else {
response = 'Error'
}
JsonRoutes.sendResult(res, {
data: response,
})
})
JsonRoutes.add('get', '/api/blockheight', (req, res) => {
let response = {}
const queryResults = status.findOne()
if (queryResults !== undefined) {
// cached transaction located
const blockheight = parseInt(queryResults.node_info.block_height, 10)
response = { found: true, blockheight }
} else {
response = { found: false, message: 'API error', code: 5002 }
}
JsonRoutes.sendResult(res, {
data: response,
})
})
JsonRoutes.add('get', '/api/blockheight/text', (req, res) => {
let response = {}
const queryResults = status.findOne()
if (queryResults !== undefined) {
// cached transaction located
const blockheight = parseInt(queryResults.node_info.block_height, 10)
response = blockheight
} else {
response = 'Error'
}
JsonRoutes.sendResult(res, {
data: response,
})
})
JsonRoutes.add('get', '/api/status', (req, res) => {
let response = {}
const queryResults = status.findOne()
if (queryResults !== undefined) {
// cached transaction located
response = queryResults
} else {
response = { found: false, message: 'API error', code: 5003 }
}
JsonRoutes.sendResult(res, {
data: response,
})
})
JsonRoutes.add('get', '/api/miningstats', (req, res) => {
let response = {}
const queryResults = homechart.findOne()
if (queryResults !== undefined) {
response = {
block: queryResults.labels[queryResults.labels.length - 1],
hashrate: queryResults.datasets[0].data[queryResults.labels.length - 1],
difficulty: queryResults.datasets[1].data[queryResults.labels.length - 1],
blocktime: queryResults.datasets[2].data[queryResults.labels.length - 1],
}
} else {
response = { found: false, message: 'API error', code: 5003 }
}
JsonRoutes.sendResult(res, {
data: response,
})
})