├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app.js ├── app ├── api │ ├── coreApi.js │ ├── electrumApi.js │ ├── mockApi.js │ └── rpcApi.js ├── coins.js ├── coins │ ├── bsv.js │ ├── btc.js │ └── ltc.js ├── config.js ├── credentials.js └── utils.js ├── bin └── www ├── docs ├── Server-Setup.md └── btc-explorer.com.conf ├── package-lock.json ├── package.json ├── public ├── css │ ├── bootstrap-dark.css │ ├── radial-progress.less │ └── styling.css └── img │ ├── logo │ ├── bchsv.png │ ├── bchsv.svg │ ├── bsv.png │ ├── bsv.svg │ ├── btc.png │ ├── btc.svg │ ├── lightning.svg │ ├── ltc.png │ └── ltc.svg │ ├── qr-btc.png │ ├── qr-ltc.png │ └── screenshots │ ├── block.png │ ├── blocks.png │ ├── connect.png │ ├── home.png │ ├── mempool-summary.png │ ├── node-details.png │ ├── rpc-browser.png │ ├── transaction-raw.png │ └── transaction.png ├── routes └── baseActionsRouter.js └── views ├── about.pug ├── address.pug ├── block.pug ├── blocks.pug ├── browser.pug ├── connect.pug ├── error.pug ├── fun.pug ├── includes ├── block-content.pug ├── blocks-list.pug ├── electrum-trust-note.pug ├── graph.pug ├── pagination.pug ├── radial-progress-bar.pug ├── time-ago.pug ├── tools-card.pug ├── transaction-io-details.pug └── value-display.pug ├── index.pug ├── layout.pug ├── mempool-summary.pug ├── node-status.pug ├── notifications.pug ├── peers.pug ├── search.pug ├── terminal.pug ├── transaction.pug ├── tx-stats.pug └── unconfirmed-transactions.pug /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | .history 60 | 61 | public/css/radial-progress.css 62 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8 2 | WORKDIR /workspace 3 | COPY . . 4 | RUN npm install 5 | CMD npm start 6 | EXPOSE 3002 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dan Janosik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WhatsOnChain Blockchain Explorer 2 | 3 | Simple, database-free SV blockchain explorer, via RPC. Built with Node.js, express, bootstrap-v4. 4 | 5 | This tool is intended to be a simple, self-hosted explorer for the Bitcoin blockchain, driven by RPC calls. 6 | 7 | Live demo available at: 8 | 9 | * BSV: https://whatsonchain.com 10 | 11 | # Features 12 | 13 | * Browse blocks 14 | * View block details 15 | * View transaction details, with navigation "backward" via spent transaction outputs 16 | * View JSON content used to generate most pages 17 | * Search supports transactions, blocks, addresses 18 | * Mempool summary, with fee, size, and age breakdowns 19 | 20 | ## Prerequisites 21 | 22 | 1. Install and run a full, archiving node - https://github.com/bitcoin-sv/bitcoin-sv. Ensure that your node has full transaction indexing enabled (`txindex=1`) and the RPC server enabled (`server=1`). 23 | 2. Synchronize your node with the Bitcoin network. 24 | 3. "Recent" version of Node.js (8+ recommended). 25 | 26 | ## Instructions 27 | 28 | 1. Clone this repo: `git clone https://github.com/waqas64/btc-rpc-explorer` 29 | 2. `npm install` 30 | 3. `npm run build` 31 | 4. Edit the "rpc" settings in [app/credentials.js](app/credentials.js) to target your node 32 | 5. Optional: Change the "coin" value in [app/config.js](app/config.js). 33 | 6. Optional: Add an ipstack.com API access key to [app/credentials.js](app/credentials.js). Doing so will add a map to the /peers page. 34 | 7. `npm start` to start the local server 35 | 8. Visit http://127.0.0.1:3002/ 36 | 37 | ## Run via Docker 38 | 39 | 1. `docker build -t btc-rpc-explorer .` 40 | 2. `docker run -p 3002:3002 -it btc-rpc-explorer` 41 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var express = require('express'); 6 | var path = require('path'); 7 | var favicon = require('serve-favicon'); 8 | var logger = require('morgan'); 9 | var cookieParser = require('cookie-parser'); 10 | var bodyParser = require('body-parser'); 11 | var session = require("express-session"); 12 | var config = require("./app/config.js"); 13 | var simpleGit = require('simple-git'); 14 | var utils = require("./app/utils.js"); 15 | var moment = require("moment"); 16 | var Decimal = require('decimal.js'); 17 | var bitcoinCore = require("bitcoin-core"); 18 | var pug = require("pug"); 19 | var momentDurationFormat = require("moment-duration-format"); 20 | var coreApi = require("./app/api/coreApi.js"); 21 | var coins = require("./app/coins.js"); 22 | var request = require("request"); 23 | var qrcode = require("qrcode"); 24 | var fs = require('fs'); 25 | var electrumApi = require("./app/api/electrumApi.js"); 26 | 27 | var crawlerBotUserAgentStrings = [ "Googlebot", "Bingbot", "Slurp", "DuckDuckBot", "Baiduspider", "YandexBot", "Sogou", "Exabot", "facebot", "ia_archiver" ]; 28 | 29 | 30 | var baseActionsRouter = require('./routes/baseActionsRouter'); 31 | 32 | var app = express(); 33 | 34 | // view engine setup 35 | app.set('views', path.join(__dirname, 'views')); 36 | 37 | // ref: https://blog.stigok.com/post/disable-pug-debug-output-with-expressjs-web-app 38 | app.engine('pug', (path, options, fn) => { 39 | options.debug = false; 40 | return pug.__express.call(null, path, options, fn); 41 | }); 42 | 43 | app.set('view engine', 'pug'); 44 | 45 | // uncomment after placing your favicon in /public 46 | //app.use(favicon(__dirname + '/public/favicon.ico')); 47 | app.use(logger('dev')); 48 | app.use(bodyParser.json()); 49 | app.use(bodyParser.urlencoded({ extended: false })); 50 | app.use(cookieParser()); 51 | app.use(session({ 52 | secret: config.cookiePassword, 53 | resave: false, 54 | saveUninitialized: false 55 | })); 56 | app.use(express.static(path.join(__dirname, 'public'))); 57 | 58 | process.on("unhandledRejection", (reason, p) => { 59 | console.log("Unhandled Rejection at: Promise", p, "reason:", reason, "stack:", reason.stack); 60 | }); 61 | 62 | 63 | 64 | app.runOnStartup = function() { 65 | global.config = config; 66 | global.coinConfig = coins[config.coin]; 67 | global.coinConfigs = coins; 68 | 69 | console.log("Running RPC Explorer for " + global.coinConfig.name); 70 | 71 | var rpcCredentials = null; 72 | if (config.credentials.rpc) { 73 | rpcCredentials = config.credentials.rpc; 74 | 75 | } else if (process.env.RPC_HOST) { 76 | rpcCredentials = { 77 | host: process.env.RPC_HOST, 78 | port: process.env.RPC_PORT, 79 | username: process.env.RPC_USERNAME, 80 | password: process.env.RPC_PASSWORD 81 | }; 82 | } 83 | 84 | if (rpcCredentials) { 85 | console.log("Connecting via RPC to node at " + config.credentials.rpc.host + ":" + config.credentials.rpc.port); 86 | 87 | global.client = new bitcoinCore({ 88 | host: rpcCredentials.host, 89 | port: rpcCredentials.port, 90 | username: rpcCredentials.username, 91 | password: rpcCredentials.password, 92 | timeout: 20000 93 | }); 94 | } 95 | 96 | if (config.donationAddresses) { 97 | var getDonationAddressQrCode = function(coinId) { 98 | qrcode.toDataURL(config.donationAddresses[coinId].address, function(err, url) { 99 | global.donationAddressQrCodeUrls[coinId] = url; 100 | }); 101 | }; 102 | 103 | global.donationAddressQrCodeUrls = {}; 104 | 105 | config.donationAddresses.coins.forEach(function(item) { 106 | getDonationAddressQrCode(item); 107 | }); 108 | } 109 | 110 | global.specialTransactions = {}; 111 | global.specialBlocks = {}; 112 | global.specialAddresses = {}; 113 | 114 | if (config.donationAddresses && config.donationAddresses[coinConfig.ticker]) { 115 | global.specialAddresses[config.donationAddresses[coinConfig.ticker].address] = {type:"donation"}; 116 | } 117 | 118 | if (global.coinConfig.historicalData) { 119 | global.coinConfig.historicalData.forEach(function(item) { 120 | if (item.type == "blockheight") { 121 | global.specialBlocks[item.blockHash] = item; 122 | 123 | } else if (item.type == "tx") { 124 | global.specialTransactions[item.txid] = item; 125 | 126 | } else if (item.type == "address") { 127 | global.specialAddresses[item.address] = {type:"fun", addressInfo:item}; 128 | } 129 | }); 130 | } 131 | 132 | if (config.electrumXServers && config.electrumXServers.length > 0) { 133 | electrumApi.connectToServers().then(function() { 134 | console.log("Live with ElectrumX API."); 135 | 136 | global.electrumApi = electrumApi; 137 | 138 | }).catch(function(err) { 139 | console.log("Error 31207ugf4e0fed: " + err + ", while initializing ElectrumX API"); 140 | }); 141 | } 142 | 143 | if (global.coinConfig.miningPoolsConfigUrls) { 144 | var promises = []; 145 | 146 | for (var i = 0; i < global.coinConfig.miningPoolsConfigUrls.length; i++) { 147 | promises.push(new Promise(function(resolve, reject) { 148 | request(global.coinConfig.miningPoolsConfigUrls[i], function(error, response, body) { 149 | if (!error && response && response.statusCode && response.statusCode == 200) { 150 | var responseBody = JSON.parse(body); 151 | 152 | resolve(responseBody); 153 | 154 | } else { 155 | console.log("Error:"); 156 | console.log(error); 157 | console.log("Response:"); 158 | console.log(response); 159 | 160 | resolve({"coinbase_tags" : {}, "payout_addresses":{}}); 161 | } 162 | }); 163 | })); 164 | } 165 | 166 | Promise.all(promises).then(function(results) { 167 | global.miningPoolsConfigs = results; 168 | 169 | for (var i = 0; i < global.miningPoolsConfigs.length; i++) { 170 | for (var x in global.miningPoolsConfigs[i].payout_addresses) { 171 | if (global.miningPoolsConfigs[i].payout_addresses.hasOwnProperty(x)) { 172 | global.specialAddresses[x] = {type:"minerPayout", minerInfo:global.miningPoolsConfigs[i].payout_addresses[x]}; 173 | } 174 | } 175 | } 176 | }); 177 | } 178 | 179 | // if (global.sourcecodeVersion == null) { 180 | // simpleGit(".").log(["-n 1"], function(err, log) { 181 | // global.sourcecodeVersion = log.all[0].hash.substring(0, 10); 182 | // global.sourcecodeDate = log.all[0].date.substring(0, "0000-00-00".length); 183 | // }); 184 | // } 185 | 186 | if (global.exchangeRate == null) { 187 | utils.refreshExchangeRate(); 188 | } 189 | 190 | // refresh exchange rate periodically 191 | setInterval(utils.refreshExchangeRate, 1800000); 192 | 193 | utils.logMemoryUsage(); 194 | setInterval(utils.logMemoryUsage, 5000); 195 | }; 196 | 197 | app.use(function(req, res, next) { 198 | // make session available in templates 199 | res.locals.session = req.session; 200 | 201 | if (config.credentials.rpc && req.session.host == null) { 202 | req.session.host = config.credentials.rpc.host; 203 | req.session.port = config.credentials.rpc.port; 204 | req.session.username = config.credentials.rpc.username; 205 | } 206 | 207 | var userAgent = req.headers['user-agent']; 208 | for (var i = 0; i < crawlerBotUserAgentStrings.length; i++) { 209 | if (userAgent.indexOf(crawlerBotUserAgentStrings[i]) != -1) { 210 | res.locals.crawlerBot = true; 211 | } 212 | } 213 | 214 | res.locals.config = global.config; 215 | res.locals.coinConfig = global.coinConfig; 216 | 217 | res.locals.host = req.session.host; 218 | res.locals.port = req.session.port; 219 | 220 | res.locals.genesisBlockHash = coreApi.getGenesisBlockHash(); 221 | res.locals.genesisCoinbaseTransactionId = coreApi.getGenesisCoinbaseTransactionId(); 222 | 223 | 224 | // currency format type 225 | if (!req.session.currencyFormatType) { 226 | var cookieValue = req.cookies['user-setting-currencyFormatType']; 227 | 228 | if (cookieValue) { 229 | req.session.currencyFormatType = cookieValue; 230 | 231 | } else { 232 | req.session.currencyFormatType = ""; 233 | } 234 | } 235 | 236 | // theme 237 | if (!req.session.uiTheme) { 238 | var cookieValue = req.cookies['user-setting-uiTheme']; 239 | 240 | if (cookieValue) { 241 | req.session.uiTheme = cookieValue; 242 | 243 | } else { 244 | req.session.uiTheme = ""; 245 | } 246 | } 247 | 248 | // homepage banner 249 | if (!req.session.hideHomepageBanner) { 250 | var cookieValue = req.cookies['user-setting-hideHomepageBanner']; 251 | 252 | if (cookieValue) { 253 | req.session.hideHomepageBanner = cookieValue; 254 | 255 | } else { 256 | req.session.hideHomepageBanner = "false"; 257 | } 258 | } 259 | 260 | // electrum trust warnings on address pages 261 | if (!req.session.hideElectrumTrustWarnings) { 262 | var cookieValue = req.cookies['user-setting-hideElectrumTrustWarnings']; 263 | 264 | if (cookieValue) { 265 | req.session.hideElectrumTrustWarnings = cookieValue; 266 | 267 | } else { 268 | req.session.hideElectrumTrustWarnings = "false"; 269 | } 270 | } 271 | 272 | res.locals.currencyFormatType = req.session.currencyFormatType; 273 | 274 | 275 | if (!["/", "/connect"].includes(req.originalUrl)) { 276 | if (utils.redirectToConnectPageIfNeeded(req, res)) { 277 | return; 278 | } 279 | } 280 | 281 | if (req.session.userMessage) { 282 | res.locals.userMessage = req.session.userMessage; 283 | 284 | if (req.session.userMessageType) { 285 | res.locals.userMessageType = req.session.userMessageType; 286 | 287 | } else { 288 | res.locals.userMessageType = "warning"; 289 | } 290 | 291 | req.session.userMessage = null; 292 | req.session.userMessageType = null; 293 | } 294 | 295 | if (req.session.query) { 296 | res.locals.query = req.session.query; 297 | 298 | req.session.query = null; 299 | } 300 | 301 | // make some var available to all request 302 | // ex: req.cheeseStr = "cheese"; 303 | 304 | next(); 305 | }); 306 | 307 | app.use('/', baseActionsRouter); 308 | 309 | /// catch 404 and forwarding to error handler 310 | app.use(function(req, res, next) { 311 | var err = new Error('Not Found'); 312 | err.status = 404; 313 | next(err); 314 | }); 315 | 316 | /// error handlers 317 | 318 | // development error handler 319 | // will print stacktrace 320 | if (app.get('env') === 'development') { 321 | app.use(function(err, req, res, next) { 322 | res.status(err.status || 500); 323 | res.render('error', { 324 | message: err.message, 325 | error: err 326 | }); 327 | }); 328 | } 329 | 330 | // production error handler 331 | // no stacktraces leaked to user 332 | app.use(function(err, req, res, next) { 333 | res.status(err.status || 500); 334 | res.render('error', { 335 | message: err.message, 336 | error: {} 337 | }); 338 | }); 339 | 340 | app.locals.moment = moment; 341 | app.locals.Decimal = Decimal; 342 | app.locals.utils = utils; 343 | 344 | 345 | 346 | module.exports = app; 347 | -------------------------------------------------------------------------------- /app/api/electrumApi.js: -------------------------------------------------------------------------------- 1 | var config = require("./../config.js"); 2 | var coins = require("../coins.js"); 3 | var utils = require("../utils.js"); 4 | 5 | var coinConfig = coins[config.coin]; 6 | 7 | const ElectrumClient = require('electrum-client'); 8 | 9 | var electrumClients = []; 10 | 11 | function connectToServers() { 12 | return new Promise(function(resolve, reject) { 13 | var promises = []; 14 | 15 | for (var i = 0; i < config.electrumXServers.length; i++) { 16 | var { host, port, protocol } = config.electrumXServers[i]; 17 | promises.push(connectToServer(host, port, protocol)); 18 | } 19 | 20 | Promise.all(promises).then(function() { 21 | resolve(); 22 | 23 | }).catch(function(err) { 24 | console.log("Error 120387rygxx231gwe40: " + err); 25 | 26 | reject(err); 27 | }); 28 | }); 29 | } 30 | 31 | function reconnectToServers() { 32 | return new Promise(function(resolve, reject) { 33 | for (var i = 0; i < electrumClients.length; i++) { 34 | electrumClients[i].close(); 35 | } 36 | 37 | electrumClients = []; 38 | 39 | console.log("Reconnecting ElectrumX sockets..."); 40 | 41 | connectToServers().catch(function(err) { 42 | console.log("Error 317fh29y7fg3333: " + err); 43 | 44 | }).finally(function() { 45 | console.log("Done reconnecting ElectrumX sockets."); 46 | 47 | resolve(); 48 | }); 49 | }); 50 | } 51 | 52 | function connectToServer(host, port, protocol) { 53 | return new Promise(function(resolve, reject) { 54 | console.log("Connecting to ElectrumX Server: " + host + ":" + port); 55 | 56 | // default protocol is 'tcp' if port is 50001, which is the default unencrypted port for electrumx 57 | var defaultProtocol = port === 50001 ? 'tcp' : 'tls'; 58 | var electrumClient = new ElectrumClient(port, host, protocol || defaultProtocol); 59 | electrumClient.initElectrum({client:"btc-rpc-explorer-v1.1", version:"1.2"}).then(function(res) { 60 | console.log("Connected to ElectrumX Server: " + host + ":" + port + ", versions: " + res); 61 | 62 | electrumClients.push(electrumClient); 63 | 64 | resolve(); 65 | 66 | }).catch(function(err) { 67 | console.log("Error 137rg023xx7gerfwdd: " + err + ", when trying to connect to ElectrumX server at " + host + ":" + port); 68 | 69 | reject(err); 70 | }); 71 | }); 72 | 73 | } 74 | 75 | function runOnServer(electrumClient, f) { 76 | return new Promise(function(resolve, reject) { 77 | f(electrumClient).then(function(result) { 78 | if (result.success) { 79 | resolve({result:result.response, server:electrumClient.host}); 80 | 81 | } else { 82 | reject({error:result.error, server:electrumClient.host}); 83 | } 84 | }).catch(function(err) { 85 | console.log("Error dif0e21qdh: " + err + ", host=" + electrumClient.host + ", port=" + electrumClient.port); 86 | 87 | reject(err); 88 | }); 89 | }); 90 | } 91 | 92 | function runOnAllServers(f) { 93 | return new Promise(function(resolve, reject) { 94 | var promises = []; 95 | 96 | for (var i = 0; i < electrumClients.length; i++) { 97 | promises.push(runOnServer(electrumClients[i], f)); 98 | } 99 | 100 | Promise.all(promises).then(function(results) { 101 | resolve(results); 102 | 103 | }).catch(function(err) { 104 | reject(err); 105 | }); 106 | }); 107 | } 108 | 109 | function getAddressTxids(addrScripthash) { 110 | return new Promise(function(resolve, reject) { 111 | runOnAllServers(function(electrumClient) { 112 | return electrumClient.blockchainScripthash_getHistory(addrScripthash); 113 | 114 | }).then(function(results) { 115 | if (addrScripthash == coinConfig.genesisCoinbaseOutputAddressScripthash) { 116 | for (var i = 0; i < results.length; i++) { 117 | results[i].result.unshift({tx_hash:coinConfig.genesisCoinbaseTransactionId, height:0}); 118 | } 119 | } 120 | 121 | var first = results[0]; 122 | var done = false; 123 | 124 | for (var i = 1; i < results.length; i++) { 125 | if (results[i].length != first.length) { 126 | resolve({conflictedResults:results}); 127 | 128 | done = true; 129 | } 130 | } 131 | 132 | if (!done) { 133 | resolve(results[0]); 134 | } 135 | }).catch(function(err) { 136 | reject(err); 137 | }); 138 | }); 139 | } 140 | 141 | function getAddressBalance(addrScripthash) { 142 | return new Promise(function(resolve, reject) { 143 | runOnAllServers(function(electrumClient) { 144 | return electrumClient.blockchainScripthash_getBalance(addrScripthash); 145 | 146 | }).then(function(results) { 147 | if (addrScripthash == coinConfig.genesisCoinbaseOutputAddressScripthash) { 148 | for (var i = 0; i < results.length; i++) { 149 | var coinbaseBlockReward = coinConfig.blockRewardFunction(0); 150 | 151 | results[i].result.confirmed += (coinbaseBlockReward * coinConfig.baseCurrencyUnit.multiplier); 152 | } 153 | } 154 | 155 | var first = results[0]; 156 | var done = false; 157 | 158 | for (var i = 1; i < results.length; i++) { 159 | if (results[i].confirmed != first.confirmed) { 160 | resolve({conflictedResults:results}); 161 | 162 | done = true; 163 | } 164 | } 165 | 166 | if (!done) { 167 | resolve(results[0]); 168 | } 169 | }).catch(function(err) { 170 | reject(err); 171 | }); 172 | }); 173 | } 174 | 175 | module.exports = { 176 | connectToServers: connectToServers, 177 | reconnectToServers: reconnectToServers, 178 | getAddressTxids: getAddressTxids, 179 | getAddressBalance: getAddressBalance 180 | }; -------------------------------------------------------------------------------- /app/api/mockApi.js: -------------------------------------------------------------------------------- 1 | var utils = require("../utils.js"); 2 | var config = require("../config.js"); 3 | var coins = require("../coins.js"); 4 | 5 | var SHA256 = require("crypto-js/sha256"); 6 | var earliestBlockTime = 1231006505; 7 | var avgBlockTime = 200000; 8 | var currentBlockHeight = 1234567; 9 | 10 | 11 | function getBlockchainInfo() { 12 | return new Promise(function(resolve, reject) { 13 | resolve({ 14 | blocks: currentBlockHeight 15 | }); 16 | }); 17 | } 18 | 19 | function getNetworkInfo() { 20 | return getRpcData("getnetworkinfo"); 21 | } 22 | 23 | function getNetTotals() { 24 | return getRpcData("getnettotals"); 25 | } 26 | 27 | function getMempoolInfo() { 28 | return getRpcData("getmempoolinfo"); 29 | } 30 | 31 | function getUptimeSeconds() { 32 | return getRpcData("uptime"); 33 | } 34 | 35 | function getRawMempool() { 36 | return getRpcDataWithParams("getrawmempool", true); 37 | } 38 | 39 | function getBlockByHeight(blockHeight) { 40 | var txCount = utils.seededRandomIntBetween(blockHeight, 1, 20); 41 | var txids = []; 42 | for (var i = 0; i < txCount; i++) { 43 | txids.push(SHA256("" + blockHeight + "_" + i)); 44 | } 45 | 46 | return new Promise(function(resolve, reject) { 47 | resolve({ 48 | "hash": SHA256("" + blockHeight), 49 | "confirmations": currentBlockHeight - blockHeight, 50 | "strippedsize": 56098, 51 | "size": 65384, 52 | "weight": 233678, 53 | "height": blockHeight, 54 | "version": 536870912, 55 | "versionHex": "20000000", 56 | "merkleroot": "567a3d773b07372179ad651edc02776f851020af69b7375a68ad89557dcbff5b", 57 | "tx": txids, 58 | "time": 1529848136, 59 | "mediantime": 1529846560, 60 | "nonce": 3615953854, 61 | "bits": "17376f56", 62 | "difficulty": "5077499034879.017", 63 | "chainwork": SHA256("xyz" + blockHeight), 64 | "previousblockhash": SHA256("" + (blockHeight - 1)), 65 | "nextblockhash": SHA256("" + (blockHeight + 1)) 66 | }); 67 | }); 68 | } 69 | 70 | function getBlocksByHeight(blockHeights) { 71 | console.log("mock.getBlocksByHeight: " + blockHeights); 72 | return new Promise(function(resolve, reject) { 73 | var blocks = []; 74 | for (var i = 0; i < blockHeights.length; i++) { 75 | getBlockByHeight(blockHeights[i]).then(function(result) { 76 | blocks.push(result); 77 | }); 78 | /*blocks.push({ 79 | "hash": "000000000000000000001542470d8261b9e5a2c3c2be2e2ab292d1a4c8250b12", 80 | "confirmations": 3, 81 | "strippedsize": 56098, 82 | "size": 65384, 83 | "weight": 233678, 84 | "height": blockHeights[i], 85 | "version": 536870912, 86 | "versionHex": "20000000", 87 | "merkleroot": "567a3d773b07372179ad651edc02776f851020af69b7375a68ad89557dcbff5b", 88 | "tx": [ 89 | "a97a04ebcaaca0ec80a6b2f295171eb8b082b4bc5446cd085444c304dca6f014", 90 | "223fdd9cae01f3253adc0f0133cc8e6bebdb6f1481dfa0cd9cbfebff656f32f8", 91 | "e999b2b8f1ee1e0b1adcc138d96d16e4fb65f2b422fc08d59a3b306b6a5c73d6", 92 | "328ae013c7870ab29ffd93e1a1c01db6205229f261a91a04e73539c99861923f", 93 | "b0604a447db9a0170a10a8d6cd2d68258783ae3061e5bfe5e26bcb6e76728c08", 94 | ], 95 | "time": 1529848136, 96 | "mediantime": 1529846560, 97 | "nonce": 3615953854, 98 | "bits": "17376f56", 99 | "difficulty": "5077499034879.017", 100 | "chainwork": "00000000000000000000000000000000000000000226420affb91a60111258b4", 101 | "previousblockhash": "0000000000000000003147c5229962ca4e38714fc5aee8cf38670cf1a4ef297b", 102 | "nextblockhash": "0000000000000000003382a0eef5b127c5d5ea270c85d9db3f3c605d32287cc5" 103 | });*/ 104 | } 105 | 106 | resolve(blocks); 107 | }); 108 | } 109 | 110 | function getBlockByHash(blockHash) { 111 | return new Promise(function(resolve, reject) { 112 | resolve({ 113 | "hash": blockHash, 114 | "confirmations": 3, 115 | "strippedsize": 56098, 116 | "size": 65384, 117 | "weight": 233678, 118 | "height": 123456, 119 | "version": 536870912, 120 | "versionHex": "20000000", 121 | "merkleroot": "567a3d773b07372179ad651edc02776f851020af69b7375a68ad89557dcbff5b", 122 | "tx": [ 123 | "a97a04ebcaaca0ec80a6b2f295171eb8b082b4bc5446cd085444c304dca6f014", 124 | "223fdd9cae01f3253adc0f0133cc8e6bebdb6f1481dfa0cd9cbfebff656f32f8", 125 | "e999b2b8f1ee1e0b1adcc138d96d16e4fb65f2b422fc08d59a3b306b6a5c73d6", 126 | "328ae013c7870ab29ffd93e1a1c01db6205229f261a91a04e73539c99861923f", 127 | "b0604a447db9a0170a10a8d6cd2d68258783ae3061e5bfe5e26bcb6e76728c08", 128 | ], 129 | "time": 1529848136, 130 | "mediantime": 1529846560, 131 | "nonce": 3615953854, 132 | "bits": "17376f56", 133 | "difficulty": "5077499034879.017", 134 | "chainwork": "00000000000000000000000000000000000000000226420affb91a60111258b4", 135 | "previousblockhash": "0000000000000000003147c5229962ca4e38714fc5aee8cf38670cf1a4ef297b", 136 | "nextblockhash": "0000000000000000003382a0eef5b127c5d5ea270c85d9db3f3c605d32287cc5" 137 | }); 138 | }); 139 | } 140 | 141 | function getBlocksByHash(blockHashes) { 142 | return new Promise(function(resolve, reject) { 143 | var blocks = []; 144 | for (var i = 0; i < blockHashes.length; i++) { 145 | blocks.push({ 146 | "hash": blockHashes[i], 147 | "confirmations": 3, 148 | "strippedsize": 56098, 149 | "size": 65384, 150 | "weight": 233678, 151 | "height": 123456, 152 | "version": 536870912, 153 | "versionHex": "20000000", 154 | "merkleroot": "567a3d773b07372179ad651edc02776f851020af69b7375a68ad89557dcbff5b", 155 | "tx": [ 156 | "a97a04ebcaaca0ec80a6b2f295171eb8b082b4bc5446cd085444c304dca6f014", 157 | "223fdd9cae01f3253adc0f0133cc8e6bebdb6f1481dfa0cd9cbfebff656f32f8", 158 | "e999b2b8f1ee1e0b1adcc138d96d16e4fb65f2b422fc08d59a3b306b6a5c73d6", 159 | "328ae013c7870ab29ffd93e1a1c01db6205229f261a91a04e73539c99861923f", 160 | "b0604a447db9a0170a10a8d6cd2d68258783ae3061e5bfe5e26bcb6e76728c08", 161 | ], 162 | "time": 1529848136, 163 | "mediantime": 1529846560, 164 | "nonce": 3615953854, 165 | "bits": "17376f56", 166 | "difficulty": "5077499034879.017", 167 | "chainwork": "00000000000000000000000000000000000000000226420affb91a60111258b4", 168 | "previousblockhash": "0000000000000000003147c5229962ca4e38714fc5aee8cf38670cf1a4ef297b", 169 | "nextblockhash": "0000000000000000003382a0eef5b127c5d5ea270c85d9db3f3c605d32287cc5" 170 | }); 171 | } 172 | 173 | resolve(blocks); 174 | }); 175 | } 176 | 177 | function getRawTransaction(txid) { 178 | return new Promise(function(resolve, reject) { 179 | resolve({ 180 | "txid": txid, 181 | "hash": txid, 182 | "version": 1, 183 | "size": 237, 184 | "vsize": 210, 185 | "locktime": 0, 186 | "vin": [ 187 | { 188 | "coinbase": "03851208fabe6d6d7bf60491521f081d77fa018fb41a167dd447bf20e7d2487426c3cee65332cdb50100000000000000266508019fcf7fcb7b01002ffd0c2f736c7573682f", 189 | "sequence": 0 190 | } 191 | ], 192 | "vout": [ 193 | { 194 | "value": 12.51946416, 195 | "n": 0, 196 | "scriptPubKey": { 197 | "asm": "OP_DUP OP_HASH160 7c154ed1dc59609e3d26abb2df2ea3d587cd8c41 OP_EQUALVERIFY OP_CHECKSIG", 198 | "hex": "76a9147c154ed1dc59609e3d26abb2df2ea3d587cd8c4188ac", 199 | "reqSigs": 1, 200 | "type": "pubkeyhash", 201 | "addresses": [ 202 | "1CK6KHY6MHgYvmRQ4PAafKYDrg1ejbH1cE" 203 | ] 204 | } 205 | }, 206 | { 207 | "value": 0, 208 | "n": 1, 209 | "scriptPubKey": { 210 | "asm": "OP_RETURN aa21a9ed2b367f88dbcc39b83e89703d5425a9b51fa3d2d921b8f39a42bc54492b986281", 211 | "hex": "6a24aa21a9ed2b367f88dbcc39b83e89703d5425a9b51fa3d2d921b8f39a42bc54492b986281", 212 | "type": "nulldata" 213 | } 214 | } 215 | ], 216 | "hex": "010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff4503851208fabe6d6d7bf60491521f081d77fa018fb41a167dd447bf20e7d2487426c3cee65332cdb50100000000000000266508019fcf7fcb7b01002ffd0c2f736c7573682f0000000002b02f9f4a000000001976a9147c154ed1dc59609e3d26abb2df2ea3d587cd8c4188ac0000000000000000266a24aa21a9ed2b367f88dbcc39b83e89703d5425a9b51fa3d2d921b8f39a42bc54492b9862810120000000000000000000000000000000000000000000000000000000000000000000000000", 217 | "blockhash": "000000000000000000001542470d8261b9e5a2c3c2be2e2ab292d1a4c8250b12", 218 | "confirmations": 3, 219 | "time": 1529848136, 220 | "blocktime": 1529848136 221 | }); 222 | }); 223 | } 224 | 225 | function getAddress(address) { 226 | return getRpcDataWithParams("validateaddress", address); 227 | } 228 | 229 | function getRawTransactions(txids) { 230 | return new Promise(function(resolve, reject) { 231 | var txs = []; 232 | for (var i = 0; i < txids.length; i++) { 233 | txs.push({ 234 | "txid": txid, 235 | "hash": txid, 236 | "version": 1, 237 | "size": 237, 238 | "vsize": 210, 239 | "locktime": 0, 240 | "vin": [ 241 | { 242 | "coinbase": "03851208fabe6d6d7bf60491521f081d77fa018fb41a167dd447bf20e7d2487426c3cee65332cdb50100000000000000266508019fcf7fcb7b01002ffd0c2f736c7573682f", 243 | "sequence": 0 244 | } 245 | ], 246 | "vout": [ 247 | { 248 | "value": 12.51946416, 249 | "n": 0, 250 | "scriptPubKey": { 251 | "asm": "OP_DUP OP_HASH160 7c154ed1dc59609e3d26abb2df2ea3d587cd8c41 OP_EQUALVERIFY OP_CHECKSIG", 252 | "hex": "76a9147c154ed1dc59609e3d26abb2df2ea3d587cd8c4188ac", 253 | "reqSigs": 1, 254 | "type": "pubkeyhash", 255 | "addresses": [ 256 | "1CK6KHY6MHgYvmRQ4PAafKYDrg1ejbH1cE" 257 | ] 258 | } 259 | }, 260 | { 261 | "value": 0, 262 | "n": 1, 263 | "scriptPubKey": { 264 | "asm": "OP_RETURN aa21a9ed2b367f88dbcc39b83e89703d5425a9b51fa3d2d921b8f39a42bc54492b986281", 265 | "hex": "6a24aa21a9ed2b367f88dbcc39b83e89703d5425a9b51fa3d2d921b8f39a42bc54492b986281", 266 | "type": "nulldata" 267 | } 268 | } 269 | ], 270 | "hex": "010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff4503851208fabe6d6d7bf60491521f081d77fa018fb41a167dd447bf20e7d2487426c3cee65332cdb50100000000000000266508019fcf7fcb7b01002ffd0c2f736c7573682f0000000002b02f9f4a000000001976a9147c154ed1dc59609e3d26abb2df2ea3d587cd8c4188ac0000000000000000266a24aa21a9ed2b367f88dbcc39b83e89703d5425a9b51fa3d2d921b8f39a42bc54492b9862810120000000000000000000000000000000000000000000000000000000000000000000000000", 271 | "blockhash": "000000000000000000001542470d8261b9e5a2c3c2be2e2ab292d1a4c8250b12", 272 | "confirmations": 3, 273 | "time": 1529848136, 274 | "blocktime": 1529848136 275 | }); 276 | } 277 | 278 | resolve(txs); 279 | }); 280 | } 281 | 282 | function getMinerFromCoinbaseTx(tx) { 283 | return null; 284 | } 285 | 286 | function getHelp() { 287 | return new Promise(function(resolve, reject) { 288 | reject("Not implemented"); 289 | }); 290 | } 291 | 292 | function getRpcMethodHelp(methodName) { 293 | return new Promise(function(resolve, reject) { 294 | reject("Not implemented"); 295 | }); 296 | } 297 | 298 | 299 | 300 | module.exports = { 301 | getBlockchainInfo: getBlockchainInfo, 302 | getNetworkInfo: getNetworkInfo, 303 | getNetTotals: getNetTotals, 304 | getMempoolInfo: getMempoolInfo, 305 | getBlockByHeight: getBlockByHeight, 306 | getBlocksByHeight: getBlocksByHeight, 307 | getBlockByHash: getBlockByHash, 308 | getRawTransaction: getRawTransaction, 309 | getRawTransactions: getRawTransactions, 310 | getRawMempool: getRawMempool, 311 | getUptimeSeconds: getUptimeSeconds, 312 | getHelp: getHelp, 313 | getRpcMethodHelp: getRpcMethodHelp, 314 | getAddress: getAddress 315 | }; -------------------------------------------------------------------------------- /app/api/rpcApi.js: -------------------------------------------------------------------------------- 1 | var utils = require("../utils.js"); 2 | var config = require("../config.js"); 3 | var coins = require("../coins.js"); 4 | 5 | function getBlockchainInfo() { 6 | return getRpcData("getblockchaininfo"); 7 | } 8 | 9 | function getNetworkInfo() { 10 | return getRpcData("getnetworkinfo"); 11 | } 12 | 13 | function getNetTotals() { 14 | return getRpcData("getnettotals"); 15 | } 16 | 17 | function getMempoolInfo() { 18 | return getRpcData("getmempoolinfo"); 19 | } 20 | 21 | function getMiningInfo() { 22 | return getRpcData("getmininginfo"); 23 | } 24 | 25 | function getUptimeSeconds() { 26 | return getRpcData("uptime"); 27 | } 28 | 29 | function getPeerInfo() { 30 | return getRpcData("getpeerinfo"); 31 | } 32 | 33 | function getRawMempool() { 34 | return getRpcDataWithParams("getrawmempool", true); 35 | } 36 | 37 | function getChainTxStats(blockCount) { 38 | return getRpcDataWithParams("getchaintxstats", blockCount); 39 | } 40 | 41 | function getBlockByHeight(blockHeight) { 42 | return new Promise(function(resolve, reject) { 43 | getBlocksByHeight([blockHeight]) 44 | .then(function(results) { 45 | if (results && results.length > 0) { 46 | resolve(results[0]); 47 | } else { 48 | resolve(null); 49 | } 50 | }) 51 | .catch(function(err) { 52 | reject(err); 53 | }); 54 | }); 55 | } 56 | 57 | function getBlocksByHeight(blockHeights) { 58 | //console.log("getBlocksByHeight: " + blockHeights); 59 | 60 | return new Promise(function(resolve, reject) { 61 | var batch = []; 62 | for (var i = 0; i < blockHeights.length; i++) { 63 | batch.push({ 64 | method: "getblockhash", 65 | parameters: [blockHeights[i]] 66 | }); 67 | } 68 | 69 | var blockHashes = []; 70 | client.command(batch).then(responses => { 71 | responses.forEach(item => { 72 | blockHashes.push(item); 73 | }); 74 | 75 | if (blockHashes.length == batch.length) { 76 | getBlocksByHash(blockHashes).then(function(blocks) { 77 | resolve(blocks); 78 | }); 79 | } 80 | }); 81 | }); 82 | } 83 | 84 | function getBlockByHash(blockHash) { 85 | return new Promise(function(resolve, reject) { 86 | getBlocksByHash([blockHash]) 87 | .then(function(results) { 88 | if (results && results.length > 0) { 89 | resolve(results[0]); 90 | } else { 91 | resolve(null); 92 | } 93 | }) 94 | .catch(function(err) { 95 | reject(err); 96 | }); 97 | }); 98 | } 99 | 100 | function getBlocksByHash(blockHashes) { 101 | console.log("rpc.getBlocksByHash: " + blockHashes); 102 | 103 | return new Promise(function(resolve, reject) { 104 | var batch = []; 105 | for (var i = 0; i < blockHashes.length; i++) { 106 | batch.push({ 107 | method: "getblock", 108 | parameters: [blockHashes[i]] 109 | }); 110 | } 111 | 112 | var blocks = []; 113 | client.command(batch).then(responses => { 114 | responses.forEach(item => { 115 | if (item.tx) { 116 | blocks.push(item); 117 | } 118 | }); 119 | 120 | var coinbaseTxids = []; 121 | for (var i = 0; i < blocks.length; i++) { 122 | coinbaseTxids.push(blocks[i].tx[0]); 123 | } 124 | 125 | getRawTransactions(coinbaseTxids).then(function(coinbaseTxs) { 126 | for (var i = 0; i < blocks.length; i++) { 127 | blocks[i].coinbaseTx = coinbaseTxs[i]; 128 | blocks[i].totalFees = utils.getBlockTotalFeesFromCoinbaseTxAndBlockHeight( 129 | coinbaseTxs[i], 130 | blocks[i].height 131 | ); 132 | blocks[i].miner = utils.getMinerFromCoinbaseTx(coinbaseTxs[i]); 133 | } 134 | 135 | resolve(blocks); 136 | }); 137 | }); 138 | }); 139 | } 140 | 141 | function getRawTransaction(txid) { 142 | return new Promise(function(resolve, reject) { 143 | getRawTransactions([txid]) 144 | .then(function(results) { 145 | if (results && results.length > 0) { 146 | if (results[0].txid) { 147 | resolve(results[0]); 148 | } else { 149 | resolve(null); 150 | } 151 | } else { 152 | resolve(null); 153 | } 154 | }) 155 | .catch(function(err) { 156 | reject(err); 157 | }); 158 | }); 159 | } 160 | 161 | function getAddress(address) { 162 | return getRpcDataWithParams("validateaddress", address); 163 | } 164 | 165 | const getRawTransactions = txids => { 166 | const genesisCoinbaseTransactionId = coins[config.coin].genesisCoinbaseTransactionId; 167 | const genesisCoinbaseTransaction = coins[config.coin].genesisCoinbaseTransaction; 168 | 169 | return new Promise((resolve, reject) => { 170 | if (!txids || txids.length == 0) { 171 | return resolve([]); 172 | } 173 | 174 | let requests = []; 175 | let results = []; 176 | 177 | txids.forEach(async txid => { 178 | if (txid) { 179 | if (genesisCoinbaseTransactionId && txid == genesisCoinbaseTransactionId) { 180 | try { 181 | let blockchainInfoResult = await getBlockchainInfo(); 182 | let result = genesisCoinbaseTransaction; 183 | 184 | result.confirmations = blockchainInfoResult.blocks; 185 | results.push(result); 186 | } catch (err) { 187 | reject(err); 188 | } 189 | } else { 190 | requests.push({ method: "getrawtransaction", parameters: [txid, 1] }); 191 | } 192 | } 193 | }); 194 | 195 | executeBatchesSequentially(utils.splitArrayIntoChunks(requests, 100), batchResult => { 196 | results.push(batchResult); 197 | 198 | var finalResults = []; 199 | for (var i = 0; i < results.length; i++) { 200 | for (var j = 0; j < results[i].length; j++) { 201 | finalResults.push(results[i][j]); 202 | } 203 | } 204 | 205 | resolve(finalResults); 206 | }); 207 | }); 208 | }; 209 | 210 | function getHelp() { 211 | return new Promise(function(resolve, reject) { 212 | client.command("help", function(err, result, resHeaders) { 213 | if (err) { 214 | console.log("Error 32907th429ghf: " + err); 215 | 216 | reject(err); 217 | 218 | return; 219 | } 220 | 221 | var lines = result.split("\n"); 222 | var sections = []; 223 | 224 | lines.forEach(function(line) { 225 | if (line.startsWith("==")) { 226 | var sectionName = line.substring(2); 227 | sectionName = sectionName.substring(0, sectionName.length - 2).trim(); 228 | 229 | sections.push({ name: sectionName, methods: [] }); 230 | } else if (line.trim().length > 0) { 231 | var methodName = line.trim(); 232 | 233 | if (methodName.includes(" ")) { 234 | methodName = methodName.substring(0, methodName.indexOf(" ")); 235 | } 236 | 237 | sections[sections.length - 1].methods.push({ name: methodName, content: line.trim() }); 238 | } 239 | }); 240 | 241 | resolve(sections); 242 | }); 243 | }); 244 | } 245 | 246 | function getRpcMethodHelp(methodName) { 247 | return new Promise(function(resolve, reject) { 248 | client.command("help", methodName, function(err, result, resHeaders) { 249 | if (err) { 250 | console.log("Error 237hwerf07wehg: " + err); 251 | 252 | reject(err); 253 | 254 | return; 255 | } 256 | 257 | var output = {}; 258 | output.string = result; 259 | 260 | var str = result; 261 | 262 | var lines = str.split("\n"); 263 | var argumentLines = []; 264 | var catchArgs = false; 265 | lines.forEach(function(line) { 266 | if (line.trim().length == 0) { 267 | catchArgs = false; 268 | } 269 | 270 | if (catchArgs) { 271 | argumentLines.push(line); 272 | } 273 | 274 | if (line.trim() == "Arguments:" || line.trim() == "Arguments") { 275 | catchArgs = true; 276 | } 277 | }); 278 | 279 | var args = []; 280 | var argX = null; 281 | // looking for line starting with "N. " where N is an integer (1-2 digits) 282 | argumentLines.forEach(function(line) { 283 | var regex = /^([0-9]+)\.\s*"?(\w+)"?\s*\(([^,)]*),?\s*([^,)]*),?\s*([^,)]*),?\s*([^,)]*)?\s*\)\s*(.+)?$/; 284 | 285 | var match = regex.exec(line); 286 | 287 | if (match) { 288 | argX = {}; 289 | argX.name = match[2]; 290 | argX.detailsLines = []; 291 | 292 | argX.properties = []; 293 | 294 | if (match[3]) { 295 | argX.properties.push(match[3]); 296 | } 297 | 298 | if (match[4]) { 299 | argX.properties.push(match[4]); 300 | } 301 | 302 | if (match[5]) { 303 | argX.properties.push(match[5]); 304 | } 305 | 306 | if (match[6]) { 307 | argX.properties.push(match[6]); 308 | } 309 | 310 | if (match[7]) { 311 | argX.description = match[7]; 312 | } 313 | 314 | args.push(argX); 315 | } 316 | 317 | if (!match && argX) { 318 | argX.detailsLines.push(line); 319 | } 320 | }); 321 | 322 | output.args = args; 323 | 324 | resolve(output); 325 | }); 326 | }); 327 | } 328 | 329 | function getRpcData(cmd) { 330 | return new Promise(function(resolve, reject) { 331 | client.command(cmd, function(err, result, resHeaders) { 332 | if (err) { 333 | console.log("Error for RPC command '" + cmd + "': " + err); 334 | 335 | reject(err); 336 | } else { 337 | resolve(result); 338 | } 339 | }); 340 | }); 341 | } 342 | 343 | function getRpcDataWithParams(cmd, params) { 344 | return new Promise(function(resolve, reject) { 345 | client.command(cmd, params, function(err, result, resHeaders) { 346 | if (err) { 347 | console.log("Error for RPC command '" + cmd + "': " + err); 348 | 349 | reject(err); 350 | } else { 351 | resolve(result); 352 | } 353 | }); 354 | }); 355 | } 356 | 357 | function executeBatchesSequentially(batches, resultFunc) { 358 | var batchId = utils.getRandomString(20, "aA#"); 359 | 360 | console.log("Starting " + batches.length + "-item batch " + batchId + "..."); 361 | 362 | executeBatchesSequentiallyInternal(batchId, batches, 0, [], resultFunc); 363 | } 364 | 365 | function executeBatchesSequentiallyInternal( 366 | batchId, 367 | batches, 368 | currentIndex, 369 | accumulatedResults, 370 | resultFunc 371 | ) { 372 | if (currentIndex == batches.length) { 373 | console.log("Finishing batch " + batchId + "..."); 374 | 375 | resultFunc(accumulatedResults); 376 | 377 | return; 378 | } 379 | 380 | console.log( 381 | "Executing item #" + (currentIndex + 1) + " (of " + batches.length + ") for batch " + batchId 382 | ); 383 | 384 | var count = batches[currentIndex].length; 385 | 386 | client.command(batches[currentIndex]).then(function(results) { 387 | results.forEach(item => { 388 | accumulatedResults.push(item); 389 | 390 | count--; 391 | }); 392 | 393 | if (count == 0) { 394 | executeBatchesSequentiallyInternal( 395 | batchId, 396 | batches, 397 | currentIndex + 1, 398 | accumulatedResults, 399 | resultFunc 400 | ); 401 | } 402 | }); 403 | } 404 | 405 | module.exports = { 406 | getBlockchainInfo: getBlockchainInfo, 407 | getNetworkInfo: getNetworkInfo, 408 | getNetTotals: getNetTotals, 409 | getMempoolInfo: getMempoolInfo, 410 | getMiningInfo: getMiningInfo, 411 | getBlockByHeight: getBlockByHeight, 412 | getBlocksByHeight: getBlocksByHeight, 413 | getBlockByHash: getBlockByHash, 414 | getRawTransaction: getRawTransaction, 415 | getRawTransactions: getRawTransactions, 416 | getRawMempool: getRawMempool, 417 | getUptimeSeconds: getUptimeSeconds, 418 | getHelp: getHelp, 419 | getRpcMethodHelp: getRpcMethodHelp, 420 | getAddress: getAddress, 421 | getPeerInfo: getPeerInfo, 422 | getChainTxStats: getChainTxStats 423 | }; 424 | -------------------------------------------------------------------------------- /app/coins.js: -------------------------------------------------------------------------------- 1 | // var btc = require("./coins/btc.js"); 2 | // var ltc = require("./coins/ltc.js"); 3 | var bsv = require("./coins/bsv.js"); 4 | 5 | module.exports = { 6 | // "BTC": btc, 7 | // "LTC": ltc, 8 | "BSV": bsv 9 | }; -------------------------------------------------------------------------------- /app/coins/bsv.js: -------------------------------------------------------------------------------- 1 | var Decimal = require("decimal.js"); 2 | Decimal8 = Decimal.clone({ precision:8, rounding:8 }); 3 | 4 | var btcCurrencyUnits = [ 5 | { 6 | name:"BSV", 7 | multiplier:1, 8 | default:true, 9 | values:["", "bsv", "BSV"], 10 | decimalPlaces:8 11 | }, 12 | { 13 | name:"mBSV", 14 | multiplier:1000, 15 | values:["mBSV"], 16 | decimalPlaces:5 17 | }, 18 | { 19 | name:"bits", 20 | multiplier:1000000, 21 | values:["bits"], 22 | decimalPlaces:2 23 | }, 24 | { 25 | name:"sat", 26 | multiplier:100000000, 27 | values:["sat", "satoshi"], 28 | decimalPlaces:0 29 | } 30 | ]; 31 | 32 | module.exports = { 33 | name:"BSV", 34 | ticker:"BSV", 35 | logoUrl:"/img/logo/bsv.png", 36 | siteTitle:"WhatsOnChain.com", 37 | pageTitle: "BSV Explorer", 38 | siteDescriptionHtml:"whatsonchain.com - Bitcoin SV Blockchain Explorer is the genesis block.", 95 | referenceUrl: "https://en.bitcoin.it/wiki/Genesis_block" 96 | }, 97 | { 98 | type: "tx", 99 | date: "2009-01-03", 100 | txid: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", 101 | summary: "The coinbase transaction of the Genesis Block.", 102 | alertBodyHtml: "This transaction doesn't really exist! This is the coinbase transaction of the Bitcoin Genesis Block. For more background about this special-case transaction, you can read this brief discussion among some of the Bitcoin developers.", 103 | referenceUrl: "https://github.com/bitcoin/bitcoin/issues/3303" 104 | }, 105 | { 106 | type: "tx", 107 | date: "2009-10-12", 108 | txid: "7dff938918f07619abd38e4510890396b1cef4fbeca154fb7aafba8843295ea2", 109 | summary: "First bitcoin traded for fiat currency.", 110 | alertBodyHtml: "In this first-known BTC-to-fiat transaction, 5,050 BTC were exchanged for 5.02 USD, at an effective exchange rate of ~0.001 USD/BTC.", 111 | referenceUrl: "https://twitter.com/marttimalmi/status/423455561703624704" 112 | }, 113 | { 114 | type: "blockheight", 115 | date: "2017-08-24", 116 | blockHeight: 481824, 117 | blockHash: "0000000000000000001c8018d9cb3b742ef25114f27563e3fc4a1902167f9893", 118 | summary: "First SegWit block.", 119 | referenceUrl: "https://twitter.com/conio/status/900722226911219712" 120 | }, 121 | { 122 | type: "tx", 123 | date: "2017-08-24", 124 | txid: "8f907925d2ebe48765103e6845C06f1f2bb77c6adc1cc002865865eb5cfd5c1c", 125 | summary: "First SegWit transaction.", 126 | referenceUrl: "https://twitter.com/KHS9NE/status/900553902923362304" 127 | }, 128 | { 129 | type: "tx", 130 | date: "2014-06-16", 131 | txid: "143a3d7e7599557f9d63e7f224f34d33e9251b2c23c38f95631b3a54de53f024", 132 | summary: "Star Wars: A New Hope", 133 | referenceUrl: "" 134 | }, 135 | { 136 | type: "tx", 137 | date: "2010-05-22", 138 | txid: "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", 139 | summary: "The 'Bitcoin Pizza' transaction.", 140 | alertBodyHtml: "This is the famous 'Bitcoin Pizza' transaction.", 141 | referenceUrl: "https://bitcointalk.org/index.php?topic=137.0" 142 | }, 143 | { 144 | type: "tx", 145 | date: "2011-05-18", 146 | txid: "5d80a29be1609db91658b401f85921a86ab4755969729b65257651bb9fd2c10d", 147 | summary: "Destroyed bitcoin.", 148 | referenceUrl: "https://www.reddit.com/r/Bitcoin/comments/7mhoks/til_in_2011_a_user_running_a_modified_mining/" 149 | }, 150 | { 151 | type: "blockheight", 152 | date: "2009-01-12", 153 | blockHeight: 170, 154 | blockHash: "00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee", 155 | summary: "First block containing a (non-coinbase) transaction.", 156 | alertBodyHtml: "This block comes 9 days after the genesis block and is the first to contain a transfer of bitcoin. Before this block all blocks contained only coinbase transactions which mint new bitcoin.", 157 | referenceUrl: "https://bitcointalk.org/index.php?topic=91806.msg1012234#msg1012234" 158 | }, 159 | { 160 | type: "blockheight", 161 | date: "2017-08-25", 162 | blockHeight: 481947, 163 | blockHash: "00000000000000000139cb443e16442fcd07a4a0e0788dd045ee3cf268982016", 164 | summary: "First block mined that was greater than 1MB.", 165 | referenceUrl: "https://en.bit.news/bitfury-mined-first-segwit-block-size-1-mb/" 166 | }, 167 | { 168 | type: "blockheight", 169 | date: "2018-01-20", 170 | blockHeight: 505225, 171 | blockHash: "0000000000000000001bbb529c64ddf55edec8f4ebc0a0ccf1d3bb21c278bfa7", 172 | summary: "First block mined that was greater than 2MB.", 173 | referenceUrl: "https://twitter.com/BitGo/status/954998877920247808" 174 | }, 175 | { 176 | type: "tx", 177 | date: "2017-12-30", 178 | txid: "9bf8853b3a823bbfa1e54017ae11a9e1f4d08a854dcce9f24e08114f2c921182", 179 | summary: "Block reward lost", 180 | alertBodyHtml: "This coinbase transaction completely fails to collect the block's mining reward. 12.5 BTC were lost.", 181 | referenceUrl: "https://bitcoin.stackexchange.com/a/67012/3397" 182 | }, 183 | { 184 | type:"address", 185 | date:"2011-12-03", 186 | address:"1JryTePceSiWVpoNBU8SbwiT7J4ghzijzW", 187 | summary:"Brainwallet address for 'Satoshi Nakamoto'", 188 | referenceUrl:"https://twitter.com/MrHodl/status/1041448002005741568", 189 | alertBodyHtml:"This address was generated from the SHA256 hash of 'Satoshi Nakamoto' as example of the 'brainwallet' concept." 190 | } 191 | ], 192 | exchangeRateData:{ 193 | jsonUrl:"https://api.coinmarketcap.com/v2/ticker/3602/", 194 | exchangedCurrencyName:"usd", 195 | responseBodySelectorFunction:function(responseBody) { 196 | if (responseBody.data && responseBody.data.quotes) { 197 | return responseBody.data.quotes.USD.price; 198 | } 199 | 200 | return -1; 201 | } 202 | }, 203 | blockRewardFunction:function(blockHeight) { 204 | var eras = [ new Decimal8(50) ]; 205 | for (var i = 1; i < 34; i++) { 206 | var previous = eras[i - 1]; 207 | eras.push(new Decimal8(previous).dividedBy(2)); 208 | } 209 | 210 | var index = Math.floor(blockHeight / 210000); 211 | 212 | return eras[index]; 213 | } 214 | }; -------------------------------------------------------------------------------- /app/coins/btc.js: -------------------------------------------------------------------------------- 1 | var Decimal = require("decimal.js"); 2 | Decimal8 = Decimal.clone({ precision:8, rounding:8 }); 3 | 4 | var btcCurrencyUnits = [ 5 | { 6 | name:"BTC", 7 | multiplier:1, 8 | default:true, 9 | values:["", "btc", "BTC"], 10 | decimalPlaces:8 11 | }, 12 | { 13 | name:"mBTC", 14 | multiplier:1000, 15 | values:["mbtc"], 16 | decimalPlaces:5 17 | }, 18 | { 19 | name:"bits", 20 | multiplier:1000000, 21 | values:["bits"], 22 | decimalPlaces:2 23 | }, 24 | { 25 | name:"sat", 26 | multiplier:100000000, 27 | values:["sat", "satoshi"], 28 | decimalPlaces:0 29 | } 30 | ]; 31 | 32 | module.exports = { 33 | name:"Bitcoin", 34 | ticker:"BTC", 35 | logoUrl:"/img/logo/btc.svg", 36 | siteTitle:"Bitcoin Explorer", 37 | siteDescriptionHtml:"BTC Explorer is the genesis block.", 94 | referenceUrl: "https://en.bitcoin.it/wiki/Genesis_block" 95 | }, 96 | { 97 | type: "tx", 98 | date: "2009-01-03", 99 | txid: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", 100 | summary: "The coinbase transaction of the Genesis Block.", 101 | alertBodyHtml: "This transaction doesn't really exist! This is the coinbase transaction of the Bitcoin Genesis Block. For more background about this special-case transaction, you can read this brief discussion among some of the Bitcoin developers.", 102 | referenceUrl: "https://github.com/bitcoin/bitcoin/issues/3303" 103 | }, 104 | { 105 | type: "tx", 106 | date: "2009-10-12", 107 | txid: "7dff938918f07619abd38e4510890396b1cef4fbeca154fb7aafba8843295ea2", 108 | summary: "First bitcoin traded for fiat currency.", 109 | alertBodyHtml: "In this first-known BTC-to-fiat transaction, 5,050 BTC were exchanged for 5.02 USD, at an effective exchange rate of ~0.001 USD/BTC.", 110 | referenceUrl: "https://twitter.com/marttimalmi/status/423455561703624704" 111 | }, 112 | { 113 | type: "blockheight", 114 | date: "2017-08-24", 115 | blockHeight: 481824, 116 | blockHash: "0000000000000000001c8018d9cb3b742ef25114f27563e3fc4a1902167f9893", 117 | summary: "First SegWit block.", 118 | referenceUrl: "https://twitter.com/conio/status/900722226911219712" 119 | }, 120 | { 121 | type: "tx", 122 | date: "2017-08-24", 123 | txid: "8f907925d2ebe48765103e6845C06f1f2bb77c6adc1cc002865865eb5cfd5c1c", 124 | summary: "First SegWit transaction.", 125 | referenceUrl: "https://twitter.com/KHS9NE/status/900553902923362304" 126 | }, 127 | { 128 | type: "tx", 129 | date: "2014-06-16", 130 | txid: "143a3d7e7599557f9d63e7f224f34d33e9251b2c23c38f95631b3a54de53f024", 131 | summary: "Star Wars: A New Hope", 132 | referenceUrl: "" 133 | }, 134 | { 135 | type: "tx", 136 | date: "2010-05-22", 137 | txid: "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", 138 | summary: "The 'Bitcoin Pizza' transaction.", 139 | alertBodyHtml: "This is the famous 'Bitcoin Pizza' transaction.", 140 | referenceUrl: "https://bitcointalk.org/index.php?topic=137.0" 141 | }, 142 | { 143 | type: "tx", 144 | date: "2011-05-18", 145 | txid: "5d80a29be1609db91658b401f85921a86ab4755969729b65257651bb9fd2c10d", 146 | summary: "Destroyed bitcoin.", 147 | referenceUrl: "https://www.reddit.com/r/Bitcoin/comments/7mhoks/til_in_2011_a_user_running_a_modified_mining/" 148 | }, 149 | { 150 | type: "blockheight", 151 | date: "2009-01-12", 152 | blockHeight: 170, 153 | blockHash: "00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee", 154 | summary: "First block containing a (non-coinbase) transaction.", 155 | alertBodyHtml: "This block comes 9 days after the genesis block and is the first to contain a transfer of bitcoin. Before this block all blocks contained only coinbase transactions which mint new bitcoin.", 156 | referenceUrl: "https://bitcointalk.org/index.php?topic=91806.msg1012234#msg1012234" 157 | }, 158 | { 159 | type: "blockheight", 160 | date: "2017-08-25", 161 | blockHeight: 481947, 162 | blockHash: "00000000000000000139cb443e16442fcd07a4a0e0788dd045ee3cf268982016", 163 | summary: "First block mined that was greater than 1MB.", 164 | referenceUrl: "https://en.bit.news/bitfury-mined-first-segwit-block-size-1-mb/" 165 | }, 166 | { 167 | type: "blockheight", 168 | date: "2018-01-20", 169 | blockHeight: 505225, 170 | blockHash: "0000000000000000001bbb529c64ddf55edec8f4ebc0a0ccf1d3bb21c278bfa7", 171 | summary: "First block mined that was greater than 2MB.", 172 | referenceUrl: "https://twitter.com/BitGo/status/954998877920247808" 173 | }, 174 | { 175 | type: "tx", 176 | date: "2017-12-30", 177 | txid: "9bf8853b3a823bbfa1e54017ae11a9e1f4d08a854dcce9f24e08114f2c921182", 178 | summary: "Block reward lost", 179 | alertBodyHtml: "This coinbase transaction completely fails to collect the block's mining reward. 12.5 BTC were lost.", 180 | referenceUrl: "https://bitcoin.stackexchange.com/a/67012/3397" 181 | }, 182 | { 183 | type:"address", 184 | date:"2011-12-03", 185 | address:"1JryTePceSiWVpoNBU8SbwiT7J4ghzijzW", 186 | summary:"Brainwallet address for 'Satoshi Nakamoto'", 187 | referenceUrl:"https://twitter.com/MrHodl/status/1041448002005741568", 188 | alertBodyHtml:"This address was generated from the SHA256 hash of 'Satoshi Nakamoto' as example of the 'brainwallet' concept." 189 | } 190 | ], 191 | exchangeRateData:{ 192 | jsonUrl:"https://api.coinmarketcap.com/v1/ticker/Bitcoin/", 193 | exchangedCurrencyName:"usd", 194 | responseBodySelectorFunction:function(responseBody) { 195 | if (responseBody[0] && responseBody[0].price_usd) { 196 | return responseBody[0].price_usd; 197 | } 198 | 199 | return -1; 200 | } 201 | }, 202 | blockRewardFunction:function(blockHeight) { 203 | var eras = [ new Decimal8(50) ]; 204 | for (var i = 1; i < 34; i++) { 205 | var previous = eras[i - 1]; 206 | eras.push(new Decimal8(previous).dividedBy(2)); 207 | } 208 | 209 | var index = Math.floor(blockHeight / 210000); 210 | 211 | return eras[index]; 212 | } 213 | }; -------------------------------------------------------------------------------- /app/coins/ltc.js: -------------------------------------------------------------------------------- 1 | var Decimal = require("decimal.js"); 2 | Decimal8 = Decimal.clone({ precision:8, rounding:8 }); 3 | 4 | var ltcCurrencyUnits = [ 5 | { 6 | name:"LTC", 7 | multiplier:1, 8 | default:true, 9 | values:["", "ltc", "LTC"], 10 | decimalPlaces:8 11 | }, 12 | { 13 | name:"lite", 14 | multiplier:1000, 15 | values:["lite"], 16 | decimalPlaces:5 17 | }, 18 | { 19 | name:"photon", 20 | multiplier:1000000, 21 | values:["photon"], 22 | decimalPlaces:2 23 | }, 24 | { 25 | name:"litoshi", 26 | multiplier:100000000, 27 | values:["litoshi", "lit"], 28 | decimalPlaces:0 29 | } 30 | ]; 31 | 32 | module.exports = { 33 | name:"Litecoin", 34 | ticker:"LTC", 35 | logoUrl:"/img/logo/ltc.svg", 36 | siteTitle:"Litecoin Explorer", 37 | nodeTitle:"Litecoin Full Node", 38 | nodeUrl:"https://litecoin.org/", 39 | demoSiteUrl: "https://ltc.chaintools.io", 40 | miningPoolsConfigUrls:[ 41 | "https://raw.githubusercontent.com/hashstream/pools/master/pools.json", 42 | ], 43 | maxBlockWeight: 4000000, 44 | currencyUnits:ltcCurrencyUnits, 45 | currencyUnitsByName:{"LTC":ltcCurrencyUnits[0], "lite":ltcCurrencyUnits[1], "photon":ltcCurrencyUnits[2], "litoshi":ltcCurrencyUnits[3]}, 46 | baseCurrencyUnit:ltcCurrencyUnits[3], 47 | feeSatoshiPerByteBucketMaxima: [5, 10, 25, 50, 100, 150, 200, 250], 48 | genesisBlockHash: "12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2", 49 | genesisCoinbaseTransactionId: "97ddfbbae6be97fd6cdf3e7ca13232a3afff2353e29badfab7f73011edd4ced9", 50 | genesisCoinbaseTransaction: { 51 | "txid":"97ddfbbae6be97fd6cdf3e7ca13232a3afff2353e29badfab7f73011edd4ced9", 52 | "hash":"97ddfbbae6be97fd6cdf3e7ca13232a3afff2353e29badfab7f73011edd4ced9", 53 | "blockhash":"12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2", 54 | "version":1, 55 | "locktime":0, 56 | "size":199, 57 | "vsize":199, 58 | "time":1317972665, 59 | "blocktime":1317972665, 60 | "vin":[ 61 | { 62 | "prev_out":{ 63 | "hash":"0000000000000000000000000000000000000000000000000000000000000000", 64 | "n":4294967295 65 | }, 66 | "coinbase":"04ffff001d0104404e592054696d65732030352f4f63742f32303131205374657665204a6f62732c204170706c65e280997320566973696f6e6172792c2044696573206174203536" 67 | } 68 | ], 69 | "vout":[ 70 | { 71 | "value":"50.00000000", 72 | "n":0, 73 | "scriptPubKey":{ 74 | "hex":"040184710fa689ad5023690c80f3a49c8f13f8d45b8c857fbcbc8bc4a8e4d3eb4b10f4d4604fa08dce601aaf0f470216fe1b51850b4acf21b179c45070ac7b03a9 OP_CHECKSIG", 75 | "type":"pubkey", 76 | "reqSigs":1, 77 | "addresses":[ 78 | "Ler4HNAEfwYhBmGXcFP2Po1NpRUEiK8km2" 79 | ] 80 | } 81 | } 82 | ] 83 | }, 84 | historicalData: [ 85 | { 86 | type: "blockheight", 87 | date: "2011-10-07", 88 | blockHeight: 0, 89 | blockHash: "12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2", 90 | summary: "The Litecoin genesis block.", 91 | alertBodyHtml: "This is the first block in the Litecoin blockchain.", 92 | referenceUrl: "https://medium.com/@SatoshiLite/satoshilite-1e2dad89a017" 93 | }, 94 | { 95 | type: "tx", 96 | date: "2017-05-10", 97 | txid: "ce385e55fb2a73fa438426145b074f08314812fa3396472dc572b3079e26e0f9", 98 | summary: "First SegWit transaction.", 99 | referenceUrl: "https://twitter.com/satoshilite/status/862345830082138113" 100 | }, 101 | { 102 | type: "blockheight", 103 | date: "2011-10-13", 104 | blockHeight: 448, 105 | blockHash: "6995d69ce2cb7768ef27f55e02dd1772d452deb44e1716bb1dd9c29409edf252", 106 | summary: "The first block containing a (non-coinbase) transaction.", 107 | referenceUrl: "" 108 | }, 109 | { 110 | type: "link", 111 | date: "2016-05-02", 112 | url: "/rpc-browser?method=verifymessage&args%5B0%5D=Ler4HNAEfwYhBmGXcFP2Po1NpRUEiK8km2&args%5B1%5D=G7W57QZ1jevRhBp7SajpcUgJiGs998R4AdBjcIgJq5BOECh4jHNatZKCFLQeo9PvZLf60ykR32XjT4IrUi9PtCU%3D&args%5B2%5D=I%2C+Charlie+Lee%2C+am+the+creator+of+Litecoin&execute=Execute", 113 | summary: "Litecoin's Proof-of-Creator", 114 | referenceUrl: "https://medium.com/@SatoshiLite/satoshilite-1e2dad89a017" 115 | } 116 | ], 117 | exchangeRateData:{ 118 | jsonUrl:"https://api.coinmarketcap.com/v1/ticker/Litecoin/", 119 | exchangedCurrencyName:"usd", 120 | responseBodySelectorFunction:function(responseBody) { 121 | if (responseBody[0] && responseBody[0].price_usd) { 122 | return responseBody[0].price_usd; 123 | } 124 | 125 | return -1; 126 | } 127 | }, 128 | blockRewardFunction:function(blockHeight) { 129 | var eras = [ new Decimal8(50) ]; 130 | for (var i = 1; i < 34; i++) { 131 | var previous = eras[i - 1]; 132 | eras.push(new Decimal8(previous).dividedBy(2)); 133 | } 134 | 135 | var index = Math.floor(blockHeight / 840000); 136 | 137 | return eras[index]; 138 | } 139 | }; -------------------------------------------------------------------------------- /app/config.js: -------------------------------------------------------------------------------- 1 | var credentials = require("./credentials.js"); 2 | var coins = require("./coins.js"); 3 | 4 | var currentCoin = "BSV"; 5 | 6 | module.exports = { 7 | cookiePassword: "0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", 8 | demoSite: false, 9 | coin: currentCoin, 10 | 11 | rpcBlacklist:[ 12 | "addnode", 13 | "backupwallet", 14 | "bumpfee", 15 | "clearbanned", 16 | "createmultisig", 17 | "disconnectnode", 18 | "dumpprivkey", 19 | "dumpwallet", 20 | "encryptwallet", 21 | "generate", 22 | "generatetoaddress", 23 | "getaccountaddrss", 24 | "getaddressesbyaccount", 25 | "getbalance", 26 | "getnewaddress", 27 | "getrawchangeaddress", 28 | "getreceivedbyaccount", 29 | "getreceivedbyaddress", 30 | "gettransaction", 31 | "getunconfirmedbalance", 32 | "getwalletinfo", 33 | "importaddress", 34 | "importmulti", 35 | "importprivkey", 36 | "importprunedfunds", 37 | "importpubkey", 38 | "importwallet", 39 | "keypoolrefill", 40 | "listaccounts", 41 | "listaddressgroupings", 42 | "listlockunspent", 43 | "listreceivedbyaccount", 44 | "listreceivedbyaddress", 45 | "listsinceblock", 46 | "listtransactions", 47 | "listunspent", 48 | "listwallets", 49 | "lockunspent", 50 | "logging", 51 | "move", 52 | "preciousblock", 53 | "pruneblockchain", 54 | "removeprunedfunds", 55 | "rescanblockchain", 56 | "savemempool", 57 | "sendfrom", 58 | "sendmany", 59 | "sendtoaddress", 60 | "sendrawtransaction", 61 | "setaccount", 62 | "setban", 63 | "setnetworkactive", 64 | "signmessage", 65 | "signmessagewithprivatekey", 66 | "signrawtransaction", 67 | "stop", 68 | "submitblock", 69 | "verifychain", 70 | "walletlock", 71 | "walletpassphrase", 72 | "walletpassphrasechange", 73 | ], 74 | 75 | // https://uasf.saltylemon.org/electrum 76 | electrumXServers:[ 77 | // set host & port of electrum servers to connect to 78 | // protocol can be "tls" or "tcp", it defaults to "tcp" if port is 50001 and "tls" otherwise 79 | {host: "sv1.hsmiths.com", port:60004, protocol: "ssl"}, 80 | {host: "satoshi.vision.cash", port:50002, protocol: "ssl"}, 81 | // {host: "electrum.qtornado.com", port:50001, protocol: "tcp"}, 82 | // {host: "electrum.coinucopia.io", port:50001, protocol: "tcp"}, 83 | 84 | ], 85 | 86 | site: { 87 | blockTxPageSize:10, 88 | addressTxPageSize:10, 89 | txMaxInput:10, 90 | browseBlocksPageSize:20 91 | }, 92 | 93 | credentials: credentials, 94 | 95 | // Edit "ipWhitelistForRpcCommands" regex to limit access to RPC Browser / Terminal to matching IPs 96 | ipWhitelistForRpcCommands:/^(127\.0\.0\.1)?(\:\:1)?$/, 97 | 98 | siteTools:[ 99 | 100 | 101 | {name:"Node Status", url:"/node-status", desc:"Summary of this node: version, network, uptime, etc.", fontawesome:"fas fa-broadcast-tower"}, 102 | {name:"Peers", url:"/peers", desc:"Detailed info about the peers connected to this node.", fontawesome:"fas fa-sitemap"}, 103 | 104 | {name:"Browse Blocks", url:"/blocks", desc:"Browse all blocks in the blockchain.", fontawesome:"fas fa-cubes"}, 105 | {name:"Transaction Stats", url:"/tx-stats", desc:"See graphs of total transaction volume and transaction rates.", fontawesome:"fas fa-chart-bar"}, 106 | 107 | {name:"Mempool Summary", url:"/mempool-summary", desc:"Detailed summary of the current mempool for this node.", fontawesome:"fas fa-clipboard-list"}, 108 | {name:"Unconfirmed Transactions", url:"/unconfirmed-tx", desc:"Browse unconfirmed/pending transactions.", fontawesome:"fas fa-unlock-alt"}, 109 | 110 | {name:"Notifications", url:"/notifications", desc:"Get notifications on slack, twitter, telegram...", fontawesome:"fas fa-bullhorn"}, 111 | 112 | // {name:"RPC Browser", url:"/rpc-browser", desc:"Browse the RPC functionality of this node. See docs and execute commands.", fontawesome:"fas fa-book"}, 113 | // {name:"RPC Terminal", url:"/rpc-terminal", desc:"Directly execute RPCs against this node.", fontawesome:"fas fa-terminal"}, 114 | // {name:(coins[currentCoin].name + " Fun"), url:"/fun", desc:"See fun/interesting historical blockchain data.", fontawesome:"fas fa-certificate"} 115 | 116 | ], 117 | 118 | donationAddresses:{ 119 | coins:["BSV"], 120 | sites:{"BSV":"https://whatsonchain.com"}, 121 | 122 | "BSV":{address:"bitcoincash:qq7su5mghkkamjss3g87g3eejxf8f3excuekujtlev"}, 123 | }, 124 | 125 | // headerDropdownLinks: { 126 | // title:"Related Sites", 127 | // links:[ 128 | // {name: "Bitcoin Explorer", url:"https://btc.chaintools.io", imgUrl:"/img/logo/btc.svg"}, 129 | // {name: "Litecoin Explorer", url:"https://ltc.chaintools.io", imgUrl:"/img/logo/ltc.svg"}, 130 | // {name: "Lightning Explorer", url:"https://lightning.chaintools.io", imgUrl:"/img/logo/lightning.svg"}, 131 | // ] 132 | // } 133 | }; 134 | -------------------------------------------------------------------------------- /app/credentials.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // Edit "rpc" below to target your node. 4 | // You may delete this section if you wish to connect manually via the UI. 5 | 6 | rpc: { 7 | host:"localhost", 8 | port:8332, 9 | username:"username", 10 | password:"password" 11 | }, 12 | 13 | // optional: enter your api access key from ipstack.com below 14 | // to include a map of the estimated locations of your node's 15 | // peers 16 | ipStackComApiAccessKey:"", 17 | 18 | // optional: GA tracking code 19 | googleAnalyticsTrackingId:"", 20 | 21 | // optional: sentry.io error-tracking url 22 | sentryUrl:"", 23 | }; 24 | -------------------------------------------------------------------------------- /app/utils.js: -------------------------------------------------------------------------------- 1 | var Decimal = require("decimal.js"); 2 | var request = require("request"); 3 | 4 | var config = require("./config.js"); 5 | var coins = require("./coins.js"); 6 | var coinConfig = coins[config.coin]; 7 | 8 | var ipCache = {}; 9 | 10 | var memoPrefixes = [ 11 | { prefix: '365', action: 'Set name' }, 12 | { prefix: '621', action: 'Post memo' }, 13 | { prefix: '877', action: 'Reply to memo' }, 14 | { prefix: '1133', action: 'Like / tip memo' }, 15 | { prefix: '1389', action: 'Set profile text' }, 16 | { prefix: '1645', action: 'Follow user' }, 17 | { prefix: '1901', action: 'Unfollow user' }, 18 | { prefix: '2669', action: 'Set profile picture' }, 19 | // {prefix:'2925', action:'Repost memo'}, planned 20 | { prefix: '3181', action: 'Post topic message' }, 21 | { prefix: '3437', action: 'Topic follow' }, 22 | { prefix: '3693', action: 'Topic unfollow' }, 23 | { prefix: '4205', action: 'Create poll' }, 24 | { prefix: '4973', action: 'Add poll option' }, 25 | { prefix: '5229', action: 'Poll vote' } 26 | // {prefix:'9325', action:'Send money'}, planned 27 | ]; 28 | 29 | var exponentScales = [ 30 | {val:1000000000000000000000000000000000, name:"?", abbreviation:"V", exponent:"33"}, 31 | {val:1000000000000000000000000000000, name:"?", abbreviation:"W", exponent:"30"}, 32 | {val:1000000000000000000000000000, name:"?", abbreviation:"X", exponent:"27"}, 33 | {val:1000000000000000000000000, name:"yotta", abbreviation:"Y", exponent:"24"}, 34 | {val:1000000000000000000000, name:"zetta", abbreviation:"Z", exponent:"21"}, 35 | {val:1000000000000000000, name:"exa", abbreviation:"E", exponent:"18"}, 36 | {val:1000000000000000, name:"peta", abbreviation:"P", exponent:"15"}, 37 | {val:1000000000000, name:"tera", abbreviation:"T", exponent:"12"}, 38 | {val:1000000000, name:"giga", abbreviation:"G", exponent:"9"}, 39 | {val:1000000, name:"mega", abbreviation:"M", exponent:"6"}, 40 | {val:1000, name:"kilo", abbreviation:"K", exponent:"3"} 41 | ]; 42 | 43 | function redirectToConnectPageIfNeeded(req, res) { 44 | if (!req.session.host) { 45 | req.session.redirectUrl = req.originalUrl; 46 | 47 | res.redirect("/"); 48 | res.end(); 49 | 50 | return true; 51 | } 52 | 53 | return false; 54 | } 55 | 56 | function getOpReturnTags(hex){ 57 | var result = {}; 58 | 59 | var ss = hex.split(' '); 60 | //var tsp = ss[1].substring(0, 8) 61 | //var scriptBody = ss[1].substring(8) 62 | if(ss && ss.length > 1){ 63 | let msp = ss[1].split(0)[0]; 64 | var mpr = memoPrefixes.filter(p => { return p.prefix === msp }) 65 | if (mpr && mpr.length > 0) { 66 | result.tag = "memo.cash"; 67 | result.memoCashPrefix = mpr[0].prefix; 68 | result.action = mpr[0].action ; 69 | return result; 70 | } 71 | 72 | var ascii = hex2ascii(ss[1]); 73 | if(ascii.includes("yours.org") && ascii.length < 10) { 74 | result.tag = "yours.org"; 75 | return result; 76 | } 77 | } 78 | 79 | 80 | return result; 81 | } 82 | 83 | 84 | function hex2ascii(hex) { 85 | var str = ""; 86 | for (var i = 0; i < hex.length; i += 2) { 87 | str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); 88 | } 89 | 90 | return str; 91 | } 92 | 93 | function splitArrayIntoChunks(array, chunkSize) { 94 | var j = array.length; 95 | var chunks = []; 96 | 97 | for (var i = 0; i < j; i += chunkSize) { 98 | chunks.push(array.slice(i, i + chunkSize)); 99 | } 100 | 101 | return chunks; 102 | } 103 | 104 | function getRandomString(length, chars) { 105 | var mask = ''; 106 | 107 | if (chars.indexOf('a') > -1) { 108 | mask += 'abcdefghijklmnopqrstuvwxyz'; 109 | } 110 | 111 | if (chars.indexOf('A') > -1) { 112 | mask += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 113 | } 114 | 115 | if (chars.indexOf('#') > -1) { 116 | mask += '0123456789'; 117 | } 118 | 119 | if (chars.indexOf('!') > -1) { 120 | mask += '~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\'; 121 | } 122 | 123 | var result = ''; 124 | for (var i = length; i > 0; --i) { 125 | result += mask[Math.floor(Math.random() * mask.length)]; 126 | } 127 | 128 | return result; 129 | } 130 | 131 | var formatCurrencyCache = {}; 132 | 133 | function formatCurrencyAmountWithForcedDecimalPlaces(amount, formatType, forcedDecimalPlaces) { 134 | if (formatCurrencyCache[formatType]) { 135 | var dec = new Decimal(amount); 136 | dec = dec.times(formatCurrencyCache[formatType].multiplier); 137 | 138 | var decimalPlaces = formatCurrencyCache[formatType].decimalPlaces; 139 | if (decimalPlaces == 0 && dec < 1) { 140 | decimalPlaces = 5; 141 | } 142 | 143 | if (forcedDecimalPlaces >= 0) { 144 | decimalPlaces = forcedDecimalPlaces; 145 | } 146 | 147 | return addThousandsSeparators(dec.toDecimalPlaces(decimalPlaces)) + " " + formatCurrencyCache[formatType].name; 148 | } 149 | 150 | for (var x = 0; x < coins[config.coin].currencyUnits.length; x++) { 151 | var currencyUnit = coins[config.coin].currencyUnits[x]; 152 | 153 | for (var y = 0; y < currencyUnit.values.length; y++) { 154 | var currencyUnitValue = currencyUnit.values[y]; 155 | 156 | if (currencyUnitValue == formatType) { 157 | formatCurrencyCache[formatType] = currencyUnit; 158 | 159 | var dec = new Decimal(amount); 160 | dec = dec.times(currencyUnit.multiplier); 161 | 162 | var decimalPlaces = currencyUnit.decimalPlaces; 163 | if (decimalPlaces == 0 && dec < 1) { 164 | decimalPlaces = 5; 165 | } 166 | 167 | if (forcedDecimalPlaces >= 0) { 168 | decimalPlaces = forcedDecimalPlaces; 169 | } 170 | 171 | return addThousandsSeparators(dec.toDecimalPlaces(decimalPlaces)) + " " + currencyUnit.name; 172 | } 173 | } 174 | } 175 | 176 | return amount; 177 | } 178 | 179 | function formatCurrencyAmount(amount, formatType) { 180 | return formatCurrencyAmountWithForcedDecimalPlaces(amount, formatType, -1); 181 | } 182 | 183 | function formatCurrencyAmountInSmallestUnits(amount, forcedDecimalPlaces) { 184 | return formatCurrencyAmountWithForcedDecimalPlaces(amount, coins[config.coin].currencyUnits[coins[config.coin].currencyUnits.length - 1].name, forcedDecimalPlaces); 185 | } 186 | 187 | // ref: https://stackoverflow.com/a/2901298/673828 188 | function addThousandsSeparators(x) { 189 | var parts = x.toString().split("."); 190 | parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); 191 | 192 | return parts.join("."); 193 | } 194 | 195 | function formatExchangedCurrency(amount) { 196 | if (global.exchangeRate != null) { 197 | var dec = new Decimal(amount); 198 | dec = dec.times(global.exchangeRate); 199 | 200 | return addThousandsSeparators(dec.toDecimalPlaces(2)) + " " + coins[config.coin].exchangeRateData.exchangedCurrencyName; 201 | } 202 | 203 | return ""; 204 | } 205 | 206 | function seededRandom(seed) { 207 | var x = Math.sin(seed++) * 10000; 208 | return x - Math.floor(x); 209 | } 210 | 211 | function seededRandomIntBetween(seed, min, max) { 212 | var rand = seededRandom(seed); 213 | return (min + (max - min) * rand); 214 | } 215 | 216 | function logMemoryUsage() { 217 | var mbUsed = process.memoryUsage().heapUsed / 1024 / 1024; 218 | mbUsed = Math.round(mbUsed * 100) / 100; 219 | 220 | var mbTotal = process.memoryUsage().heapTotal / 1024 / 1024; 221 | mbTotal = Math.round(mbTotal * 100) / 100; 222 | 223 | //console.log("memoryUsage: heapUsed=" + mbUsed + ", heapTotal=" + mbTotal + ", ratio=" + parseInt(mbUsed / mbTotal * 100)); 224 | } 225 | 226 | function getMinerFromCoinbaseTx(tx) { 227 | if (tx == null || tx.vin == null || tx.vin.length == 0) { 228 | return null; 229 | } 230 | 231 | if (global.miningPoolsConfigs) { 232 | for (var i = 0; i < global.miningPoolsConfigs.length; i++) { 233 | var miningPoolsConfig = global.miningPoolsConfigs[i]; 234 | 235 | for (var payoutAddress in miningPoolsConfig.payout_addresses) { 236 | if (miningPoolsConfig.payout_addresses.hasOwnProperty(payoutAddress)) { 237 | if (tx.vout && tx.vout.length > 0 && tx.vout[0].scriptPubKey && tx.vout[0].scriptPubKey.addresses && tx.vout[0].scriptPubKey.addresses.length > 0) { 238 | if (tx.vout[0].scriptPubKey.addresses[0] == payoutAddress) { 239 | var minerInfo = miningPoolsConfig.payout_addresses[payoutAddress]; 240 | minerInfo.identifiedBy = "payout address " + payoutAddress; 241 | 242 | return minerInfo; 243 | } 244 | } 245 | } 246 | } 247 | 248 | for (var coinbaseTag in miningPoolsConfig.coinbase_tags) { 249 | if (miningPoolsConfig.coinbase_tags.hasOwnProperty(coinbaseTag)) { 250 | if (hex2ascii(tx.vin[0].coinbase).indexOf(coinbaseTag) != -1) { 251 | var minerInfo = miningPoolsConfig.coinbase_tags[coinbaseTag]; 252 | minerInfo.identifiedBy = "coinbase tag '" + coinbaseTag + "'"; 253 | 254 | return minerInfo; 255 | } 256 | } 257 | } 258 | } 259 | } 260 | 261 | if(tx.vin[0].coinbase) 262 | { 263 | return hex2ascii(tx.vin[0].coinbase); 264 | } 265 | 266 | return null 267 | } 268 | 269 | function getTxTotalInputOutputValues(tx, txInputs, blockHeight) { 270 | var totalInputValue = new Decimal(0); 271 | var totalOutputValue = new Decimal(0); 272 | 273 | try { 274 | if(Array.isArray(tx.vin)){ 275 | for (var i = 0; i < tx.vin.length; i++) { 276 | if (tx.vin[i].coinbase) { 277 | totalInputValue = totalInputValue.plus(new Decimal(coinConfig.blockRewardFunction(blockHeight))); 278 | 279 | } else { 280 | var txInput = txInputs[i]; 281 | 282 | if (txInput) { 283 | try { 284 | var vout = txInput.vout[tx.vin[i].vout]; 285 | if (vout.value) { 286 | totalInputValue = totalInputValue.plus(new Decimal(vout.value)); 287 | } 288 | } catch (err) { 289 | console.log("Error getting tx.totalInputValue: err=" + err + ", txid=" + tx.txid + ", index=tx.vin[" + i + "]"); 290 | } 291 | } 292 | } 293 | } 294 | 295 | for (var i = 0; i < tx.vout.length; i++) { 296 | totalOutputValue = totalOutputValue.plus(new Decimal(tx.vout[i].value)); 297 | } 298 | } 299 | } catch (err) { 300 | console.log("Error computing total input/output values for tx: err=" + err + ", tx=" + JSON.stringify(tx) + ", txInputs=" + JSON.stringify(txInputs) + ", blockHeight=" + blockHeight); 301 | } 302 | 303 | return {input:totalInputValue, output:totalOutputValue}; 304 | } 305 | 306 | function getBlockTotalFeesFromCoinbaseTxAndBlockHeight(coinbaseTx, blockHeight) { 307 | if (coinbaseTx == null) { 308 | return 0; 309 | } 310 | 311 | var blockReward = coinConfig.blockRewardFunction(blockHeight); 312 | 313 | var totalOutput = new Decimal(0); 314 | for (var i = 0; i < coinbaseTx.vout.length; i++) { 315 | var outputValue = coinbaseTx.vout[i].value; 316 | if (outputValue > 0) { 317 | totalOutput = totalOutput.plus(new Decimal(outputValue)); 318 | } 319 | } 320 | 321 | return totalOutput.minus(new Decimal(blockReward)); 322 | } 323 | 324 | function refreshExchangeRate() { 325 | if (coins[config.coin].exchangeRateData) { 326 | request(coins[config.coin].exchangeRateData.jsonUrl, function(error, response, body) { 327 | if (!error && response && response.statusCode && response.statusCode == 200) { 328 | var responseBody = JSON.parse(body); 329 | 330 | var exchangeRate = coins[config.coin].exchangeRateData.responseBodySelectorFunction(responseBody); 331 | if (exchangeRate > 0) { 332 | global.exchangeRate = exchangeRate; 333 | global.exchangeRateUpdateTime = new Date(); 334 | 335 | console.log("Using exchange rate: " + global.exchangeRate + " USD/" + coins[config.coin].name + " starting at " + global.exchangeRateUpdateTime); 336 | 337 | } else { 338 | console.log("Unable to get exchange rate data"); 339 | } 340 | } else { 341 | console.log("Error:"); 342 | console.log(error); 343 | console.log("Response:"); 344 | console.log(response); 345 | } 346 | }); 347 | } 348 | } 349 | 350 | // Uses IPStack.com API 351 | function geoLocateIpAddresses(ipAddresses) { 352 | return new Promise(function(resolve, reject) { 353 | var chunks = splitArrayIntoChunks(ipAddresses, 1); 354 | 355 | var promises = []; 356 | for (var i = 0; i < chunks.length; i++) { 357 | var ipStr = ""; 358 | for (var j = 0; j < chunks[i].length; j++) { 359 | if (j > 0) { 360 | ipStr = ipStr + ","; 361 | } 362 | 363 | ipStr = ipStr + chunks[i][j]; 364 | } 365 | 366 | if (ipCache[ipStr] != null) { 367 | promises.push(new Promise(function(resolve2, reject2) { 368 | resolve2(ipCache[ipStr]); 369 | })); 370 | 371 | } else if (config.credentials.ipStackComApiAccessKey && config.credentials.ipStackComApiAccessKey.trim().length > 0) { 372 | var apiUrl = "http://api.ipstack.com/" + ipStr + "?access_key=" + config.credentials.ipStackComApiAccessKey; 373 | promises.push(new Promise(function(resolve2, reject2) { 374 | request(apiUrl, function(error, response, body) { 375 | if (error) { 376 | reject2(error); 377 | 378 | } else { 379 | resolve2(response); 380 | } 381 | }); 382 | })); 383 | } else { 384 | promises.push(new Promise(function(resolve2, reject2) { 385 | resolve2(null); 386 | })); 387 | } 388 | } 389 | 390 | Promise.all(promises).then(function(results) { 391 | var ipDetails = {ips:[], detailsByIp:{}}; 392 | 393 | for (var i = 0; i < results.length; i++) { 394 | var res = results[i]; 395 | if (res != null && res["statusCode"] == 200) { 396 | var resBody = JSON.parse(res["body"]); 397 | var ip = resBody["ip"]; 398 | 399 | ipDetails.ips.push(ip); 400 | ipDetails.detailsByIp[ip] = resBody; 401 | 402 | if (ipCache[ip] == null) { 403 | ipCache[ip] = res; 404 | } 405 | } 406 | } 407 | 408 | resolve(ipDetails); 409 | }); 410 | }); 411 | } 412 | 413 | function parseExponentStringDouble(val) { 414 | var [lead,decimal,pow] = val.toString().split(/e|\./); 415 | return +pow <= 0 416 | ? "0." + "0".repeat(Math.abs(pow)-1) + lead + decimal 417 | : lead + ( +pow >= decimal.length ? (decimal + "0".repeat(+pow-decimal.length)) : (decimal.slice(0,+pow)+"."+decimal.slice(+pow))); 418 | } 419 | 420 | function formatLargeNumber(n, decimalPlaces) { 421 | for (var i = 0; i < exponentScales.length; i++) { 422 | var item = exponentScales[i]; 423 | 424 | var fraction = new Decimal(n / item.val); 425 | if (fraction >= 1) { 426 | return [fraction.toDecimalPlaces(decimalPlaces), item]; 427 | } 428 | } 429 | 430 | return [new Decimal(n).toDecimalPlaces(decimalPlaces), {}]; 431 | } 432 | 433 | 434 | module.exports = { 435 | redirectToConnectPageIfNeeded: redirectToConnectPageIfNeeded, 436 | hex2ascii: hex2ascii, 437 | splitArrayIntoChunks: splitArrayIntoChunks, 438 | getRandomString: getRandomString, 439 | formatCurrencyAmount: formatCurrencyAmount, 440 | formatCurrencyAmountWithForcedDecimalPlaces: formatCurrencyAmountWithForcedDecimalPlaces, 441 | formatExchangedCurrency: formatExchangedCurrency, 442 | addThousandsSeparators: addThousandsSeparators, 443 | formatCurrencyAmountInSmallestUnits: formatCurrencyAmountInSmallestUnits, 444 | seededRandom: seededRandom, 445 | seededRandomIntBetween: seededRandomIntBetween, 446 | logMemoryUsage: logMemoryUsage, 447 | getMinerFromCoinbaseTx: getMinerFromCoinbaseTx, 448 | getBlockTotalFeesFromCoinbaseTxAndBlockHeight: getBlockTotalFeesFromCoinbaseTxAndBlockHeight, 449 | refreshExchangeRate: refreshExchangeRate, 450 | parseExponentStringDouble: parseExponentStringDouble, 451 | formatLargeNumber: formatLargeNumber, 452 | geoLocateIpAddresses: geoLocateIpAddresses, 453 | getTxTotalInputOutputValues: getTxTotalInputOutputValues, 454 | getOpReturnTags: getOpReturnTags 455 | }; 456 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var debug = require('debug')('my-application'); 3 | var app = require('../app'); 4 | 5 | app.set('port', process.env.PORT || 3002); 6 | 7 | var server = app.listen(app.get('port'), function() { 8 | debug('Express server listening on port ' + server.address().port); 9 | 10 | if (app.runOnStartup) { 11 | app.runOnStartup(); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /docs/Server-Setup.md: -------------------------------------------------------------------------------- 1 | ### Setup of https://btc-explorer.com on Ubuntu 16.04 2 | 3 | apt update 4 | apt upgrade 5 | apt install git python-software-properties software-properties-common nginx gcc g++ make 6 | curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - 7 | apt install -y nodejs 8 | npm install pm2 --global 9 | add-apt-repository ppa:certbot/certbot 10 | apt update 11 | apt upgrade 12 | apt install python-certbot-nginx 13 | 14 | Copy content from [./btc-explorer.com.conf](./btc-explorer.com.conf) into `/etc/nginx/sites-available/btc-explorer.com.conf` 15 | 16 | certbot --nginx -d btc-explorer.com 17 | cd /etc/ssl/certs 18 | openssl dhparam -out dhparam.pem 4096 19 | cd /home/bitcoin 20 | git clone https://github.com/janoside/btc-rpc-explorer.git 21 | cd /home/bitcoin/btc-rpc-explorer 22 | npm install 23 | pm2 start bin/www --name "btc-rpc-explorer" 24 | -------------------------------------------------------------------------------- /docs/btc-explorer.com.conf: -------------------------------------------------------------------------------- 1 | ## http://domain.com redirects to https://domain.com 2 | server { 3 | server_name btc-explorer.com; 4 | listen 80; 5 | #listen [::]:80 ipv6only=on; 6 | 7 | location / { 8 | return 301 https://btc-explorer.com$request_uri; 9 | } 10 | } 11 | 12 | ## Serves httpS://domain.com 13 | server { 14 | server_name btc-explorer.com; 15 | listen 443 ssl http2; 16 | #listen [::]:443 ssl http2 ipv6only=on; 17 | 18 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 19 | ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; 20 | ssl_prefer_server_ciphers on; 21 | ssl_session_cache shared:SSL:10m; 22 | ssl_dhparam /etc/ssl/certs/dhparam.pem; 23 | 24 | location / { 25 | proxy_pass http://localhost:3002; 26 | proxy_http_version 1.1; 27 | proxy_set_header Upgrade $http_upgrade; 28 | proxy_set_header Connection 'upgrade'; 29 | proxy_set_header Host $host; 30 | proxy_cache_bypass $http_upgrade; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "btc-rpc-explorer", 3 | "version": "1.0.0", 4 | "description": "Explorer for Bitcoin and RPC-compatible blockchains", 5 | "private": false, 6 | "scripts": { 7 | "start": "node ./bin/www", 8 | "build": "npm-run-all build:*", 9 | "build:less": "lessc ./public/css/radial-progress.less ./public/css/radial-progress.css" 10 | }, 11 | "keywords": [ 12 | "bitcoin", 13 | "litecoin", 14 | "blockchain" 15 | ], 16 | "author": "Dan Janosik ", 17 | "license": "MIT", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/janoside/btc-rpc-explorer.git" 21 | }, 22 | "dependencies": { 23 | "bitcoin-core": "2.0.0", 24 | "bitcoinjs-lib": "3.3.2", 25 | "body-parser": "~1.18.2", 26 | "cookie-parser": "~1.4.3", 27 | "crypto-js": "3.1.9-1", 28 | "debug": "~2.6.0", 29 | "decimal.js": "7.2.3", 30 | "electrum-client": "github:chaintools/node-electrum-client#43a999036f9c5", 31 | "express": "^4.16.4", 32 | "express-session": "1.15.6", 33 | "jstransformer-markdown-it": "^2.0.0", 34 | "lru-cache": "4.1.3", 35 | "moment": "^2.21.0", 36 | "moment-duration-format": "2.2.2", 37 | "morgan": "^1.9.1", 38 | "pug": "2.0.1", 39 | "qrcode": "1.2.0", 40 | "request": "2.88.0", 41 | "serve-favicon": "^2.5.0", 42 | "simple-git": "1.92.0" 43 | }, 44 | "devDependencies": { 45 | "less": "3.8.0", 46 | "npm-run-all": "^4.1.3" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /public/css/radial-progress.less: -------------------------------------------------------------------------------- 1 | .radial-progress { 2 | @circle-size: 16px; 3 | @circle-background: #d6dadc; 4 | @circle-color: #0eb23a; 5 | @inset-size: 6px; 6 | @inset-color: #ffffff; 7 | @transition-length: 0s; 8 | @shadow: 0px 0px 0px rgba(0,0,0,0.1); 9 | 10 | width: @circle-size; 11 | height: @circle-size; 12 | display: inline-block; 13 | 14 | background-color: @circle-background; 15 | border-radius: 50%; 16 | .circle { 17 | .mask, .fill, .shadow { 18 | width: @circle-size; 19 | height: @circle-size; 20 | position: absolute; 21 | border-radius: 50%; 22 | } 23 | .shadow { 24 | box-shadow: @shadow inset; 25 | } 26 | .mask, .fill { 27 | -webkit-backface-visibility: hidden; 28 | transition: -webkit-transform @transition-length; 29 | transition: -ms-transform @transition-length; 30 | transition: transform @transition-length; 31 | border-radius: 50%; 32 | } 33 | .mask { 34 | clip: rect(0px, @circle-size, @circle-size, @circle-size/2); 35 | .fill { 36 | clip: rect(0px, @circle-size/2, @circle-size, 0px); 37 | background-color: @circle-color; 38 | } 39 | } 40 | } 41 | .inset { 42 | width: @inset-size; 43 | height: @inset-size; 44 | position: absolute; 45 | margin-left: (@circle-size - @inset-size)/2; 46 | margin-top: (@circle-size - @inset-size)/2; 47 | 48 | background-color: @inset-color; 49 | border-radius: 50%; 50 | box-shadow: @shadow; 51 | } 52 | 53 | @i: 0; 54 | @increment: 180deg / 100; 55 | .loop (@i) when (@i <= 100) { 56 | &[data-progress="@{i}"] { 57 | .circle { 58 | .mask.full, .fill { 59 | -webkit-transform: rotate(@increment * @i); 60 | -ms-transform: rotate(@increment * @i); 61 | transform: rotate(@increment * @i); 62 | } 63 | .fill.fix { 64 | -webkit-transform: rotate(@increment * @i * 2); 65 | -ms-transform: rotate(@increment * @i * 2); 66 | transform: rotate(@increment * @i * 2); 67 | } 68 | } 69 | } 70 | .loop(@i + 1); 71 | } 72 | .loop(@i); 73 | } -------------------------------------------------------------------------------- /public/css/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 14px; 3 | font-family: "Ubuntu", sans-serif; 4 | /*font: 14px 'Open Sans', "Lucida Grande", Helvetica, Arial, sans-serif;*/ 5 | } 6 | 7 | hr { 8 | margin: 5px 0 15px 0; 9 | } 10 | 11 | img.header-image { 12 | margin-top: -10px; 13 | margin-bottom: -5px; 14 | width: 30px; 15 | height: 30px; 16 | margin-right: 10px; 17 | } 18 | 19 | code, .monospace { 20 | font-family: "Source Code Pro", monospace; 21 | } 22 | 23 | .card-body { 24 | padding: 1.1em; 25 | } 26 | 27 | .details-table td { 28 | border-top: none; 29 | padding: 0.4em; 30 | padding-right: 0.6em; 31 | } 32 | 33 | .properties-header { 34 | width: 160px; 35 | text-align: right; 36 | font-weight: bold; 37 | } 38 | 39 | .popover { 40 | max-width: 1200px; 41 | } 42 | 43 | pre { 44 | white-space: pre-wrap; 45 | white-space: -moz-pre-wrap; 46 | white-space: -pre-wrap; 47 | white-space: -o-pre-wrap; 48 | word-wrap: break-word; 49 | } 50 | 51 | .word-wrap { 52 | word-wrap: break-word; 53 | word-break: break-all; 54 | } 55 | 56 | .tag { 57 | border-radius: 4px; 58 | background-color: #0275d8; 59 | color: white; 60 | padding: 2px 5px; 61 | margin-right: 4px; 62 | } 63 | 64 | .tag-memo { 65 | border-radius: 4px; 66 | background-color: #487521; 67 | color: white; 68 | padding: 2px 5px; 69 | margin-right: 4px; 70 | } 71 | 72 | 73 | #subheader a { 74 | margin-right: 20px; 75 | } 76 | 77 | .table th { 78 | border-top: none; 79 | } 80 | 81 | .dropdown-item:focus, .dropdown-item:hover { 82 | background-color: #e3e3e3; 83 | } 84 | 85 | #sub-menu a:hover { 86 | text-decoration: underline; 87 | } 88 | 89 | strong { 90 | font-weight: 500; 91 | } 92 | 93 | .summary-table-label, .summary-table-content, .summary-split-table-label, .summary-split-table-content, .tx-io-label, .tx-io-content, .tx-io-desc, .tx-io-value { 94 | position: relative; 95 | width: 100%; 96 | min-height: 1px; 97 | padding-right: 5px; 98 | padding-left: 15px; 99 | margin-bottom: 5px; 100 | } 101 | 102 | .summary-table-label, .summary-split-table-label, .tx-io-label { 103 | font-weight: bold; 104 | } 105 | 106 | .summary-table-content, .summary-split-table-content { 107 | margin-bottom: 20px; 108 | } 109 | 110 | @media (min-width: 576px) { 111 | .summary-table-label { 112 | max-width: 100%; 113 | text-align: left; 114 | } 115 | .summary-table-content { 116 | max-width: 100%; 117 | margin-bottom: 20px; 118 | } 119 | 120 | .summary-split-table-label { 121 | max-width: 100%; 122 | text-align: left; 123 | } 124 | .summary-split-table-content { 125 | max-width: 100%; 126 | margin-bottom: 20px; 127 | } 128 | 129 | .tx-io-label { max-width: 100%; } 130 | .tx-io-content { max-width: 100%; } 131 | .tx-io-desc { max-width: 100%; } 132 | .tx-io-value { max-width: 100%; } 133 | } 134 | 135 | @media (min-width: 768px) { 136 | .summary-table-label { 137 | max-width: 18%; 138 | text-align: right; 139 | } 140 | .summary-table-content { 141 | max-width: 82%; 142 | margin-bottom: 5px; 143 | } 144 | 145 | .summary-split-table-label { 146 | max-width: 36%; 147 | text-align: right; 148 | } 149 | .summary-split-table-content { 150 | max-width: 64%; 151 | margin-bottom: 5px; 152 | } 153 | 154 | .tx-io-label { max-width: 8%; } 155 | .tx-io-content { max-width: 92%; } 156 | .tx-io-desc { max-width: 60%; } 157 | .tx-io-value { max-width: 40%; text-align: right; padding-right: 25px; } 158 | } 159 | 160 | @media (min-width: 992px) { 161 | .summary-table-label { 162 | max-width: 15%; 163 | text-align: right; 164 | } 165 | .summary-table-content { 166 | max-width: 85%; 167 | margin-bottom: 5px; 168 | } 169 | 170 | .summary-split-table-label { 171 | max-width: 30%; 172 | text-align: right; 173 | } 174 | .summary-split-table-content { 175 | max-width: 70%; 176 | margin-bottom: 5px; 177 | } 178 | 179 | .tx-io-label { max-width: 11%; } 180 | .tx-io-content { max-width: 89%; } 181 | .tx-io-desc { max-width: 60%; } 182 | .tx-io-value { max-width: 40%; text-align: right; padding-right: 25px; } 183 | } 184 | 185 | @media (min-width: 1200px) { 186 | .container { 187 | max-width: 1160px; 188 | } 189 | 190 | .summary-table-label { 191 | max-width: 11%; 192 | text-align: right; 193 | } 194 | .summary-table-content { 195 | max-width: 89%; 196 | margin-bottom: 5px; 197 | } 198 | 199 | .summary-split-table-label { 200 | max-width: 22%; 201 | text-align: right; 202 | } 203 | .summary-split-table-content { 204 | max-width: 78%; 205 | margin-bottom: 5px; 206 | } 207 | 208 | .tx-io-label { max-width: 9.5%; } 209 | .tx-io-content { max-width: 90.5%; } 210 | .tx-io-desc { max-width: 61%; } 211 | .tx-io-value { max-width: 39%; text-align: right; padding-right: 25px; } 212 | } 213 | 214 | @media (min-width: 1920px) { 215 | .container { 216 | max-width: 1800px; 217 | } 218 | 219 | .summary-table-label { 220 | max-width: 8%; 221 | text-align: right; 222 | } 223 | .summary-table-content { 224 | max-width: 92%; 225 | margin-bottom: 5px; 226 | } 227 | 228 | .summary-split-table-label { 229 | max-width: 16%; 230 | text-align: right; 231 | } 232 | .summary-split-table-content { 233 | max-width: 84%; 234 | margin-bottom: 5px; 235 | } 236 | 237 | .tx-io-label { max-width: 6.5%; } 238 | .tx-io-content { max-width: 93.5%; } 239 | .tx-io-desc { max-width: 70%; } 240 | .tx-io-value { max-width: 30%; text-align: right; padding-right: 25px; } 241 | } 242 | 243 | 244 | 245 | footer { 246 | border-top: solid 5px #597FA5; 247 | border-bottom: solid 5px #597FA5; 248 | } 249 | 250 | footer a { 251 | color: #ccc; 252 | text-decoration: underline; 253 | } 254 | 255 | footer a:hover { 256 | color: white; 257 | } 258 | 259 | .table-striped>tbody>tr:nth-child(odd)>td, 260 | .table-striped>tbody>tr:nth-child(odd)>th { 261 | /*background-color: #fbfbfb;*/ 262 | } 263 | 264 | text { 265 | font-weight: 350; 266 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; 267 | font-size: 14px; 268 | } 269 | 270 | .node rect { 271 | stroke: #111; 272 | fill: #fff; 273 | stroke-width: 1.5px; 274 | } 275 | 276 | .edgePath path.path { 277 | /* stroke: #edad23; 278 | fill: #edad23; */ 279 | stroke: #111; 280 | fill: #111; 281 | stroke-width: 1.5px; 282 | } 283 | 284 | .arrowhead { 285 | stroke: #84dfaa; 286 | fill: #edad23; 287 | stroke-width: 1.5px; 288 | } -------------------------------------------------------------------------------- /public/img/logo/bchsv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waqas64/woc-explorer/0af7ad73eb0c28f02a83e85dea0c77db00c0180b/public/img/logo/bchsv.png -------------------------------------------------------------------------------- /public/img/logo/bchsv.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 10 | 24 | 27 | 29 | 34 | 35 | 50 | 52 | 54 | 56 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /public/img/logo/bsv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waqas64/woc-explorer/0af7ad73eb0c28f02a83e85dea0c77db00c0180b/public/img/logo/bsv.png -------------------------------------------------------------------------------- /public/img/logo/bsv.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 10 | 24 | 27 | 29 | 34 | 35 | 50 | 52 | 54 | 56 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /public/img/logo/btc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waqas64/woc-explorer/0af7ad73eb0c28f02a83e85dea0c77db00c0180b/public/img/logo/btc.png -------------------------------------------------------------------------------- /public/img/logo/btc.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/logo/lightning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 14 | 17 | 18 | -------------------------------------------------------------------------------- /public/img/logo/ltc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waqas64/woc-explorer/0af7ad73eb0c28f02a83e85dea0c77db00c0180b/public/img/logo/ltc.png -------------------------------------------------------------------------------- /public/img/logo/ltc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/img/qr-btc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waqas64/woc-explorer/0af7ad73eb0c28f02a83e85dea0c77db00c0180b/public/img/qr-btc.png -------------------------------------------------------------------------------- /public/img/qr-ltc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waqas64/woc-explorer/0af7ad73eb0c28f02a83e85dea0c77db00c0180b/public/img/qr-ltc.png -------------------------------------------------------------------------------- /public/img/screenshots/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waqas64/woc-explorer/0af7ad73eb0c28f02a83e85dea0c77db00c0180b/public/img/screenshots/block.png -------------------------------------------------------------------------------- /public/img/screenshots/blocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waqas64/woc-explorer/0af7ad73eb0c28f02a83e85dea0c77db00c0180b/public/img/screenshots/blocks.png -------------------------------------------------------------------------------- /public/img/screenshots/connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waqas64/woc-explorer/0af7ad73eb0c28f02a83e85dea0c77db00c0180b/public/img/screenshots/connect.png -------------------------------------------------------------------------------- /public/img/screenshots/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waqas64/woc-explorer/0af7ad73eb0c28f02a83e85dea0c77db00c0180b/public/img/screenshots/home.png -------------------------------------------------------------------------------- /public/img/screenshots/mempool-summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waqas64/woc-explorer/0af7ad73eb0c28f02a83e85dea0c77db00c0180b/public/img/screenshots/mempool-summary.png -------------------------------------------------------------------------------- /public/img/screenshots/node-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waqas64/woc-explorer/0af7ad73eb0c28f02a83e85dea0c77db00c0180b/public/img/screenshots/node-details.png -------------------------------------------------------------------------------- /public/img/screenshots/rpc-browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waqas64/woc-explorer/0af7ad73eb0c28f02a83e85dea0c77db00c0180b/public/img/screenshots/rpc-browser.png -------------------------------------------------------------------------------- /public/img/screenshots/transaction-raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waqas64/woc-explorer/0af7ad73eb0c28f02a83e85dea0c77db00c0180b/public/img/screenshots/transaction-raw.png -------------------------------------------------------------------------------- /public/img/screenshots/transaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waqas64/woc-explorer/0af7ad73eb0c28f02a83e85dea0c77db00c0180b/public/img/screenshots/transaction.png -------------------------------------------------------------------------------- /views/about.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headContent 4 | title About 5 | 6 | block content 7 | h1(class="h3") About 8 | hr 9 | 10 | p This tool is intended to be a simple, self-hosted explorer for the #{coinConfig.name} blockchain, driven by RPC calls to Bitcoin SV node. This tool is easy to run but lacks some features compared to database-backed explorers. 11 | 12 | p Special thanks to @janoside, @OmisNomis and @waqas64 13 | a(href="https://github.com/waqas64/btc-rpc-explorer") github.com/waqas64/btc-rpc-explorer 14 | 15 | //- if (config.donationAddresses) 16 | //- hr 17 | 18 | //- p If you value this tool and want to support my work on this project, please consider a small donation. Anything is appreciated! 19 | 20 | //- each coin, index in config.donationAddresses.coins 21 | //- div(style="display: inline-block;", class="text-md-center mb-3", class=(index > 0 ? "ml-md-5" : false)) 22 | //- if (config.donationAddresses[coin].urlPrefix) 23 | //- a(href=(config.donationAddresses[coin].urlPrefix + config.donationAddresses[coin].address)) #{coinConfigs[coin].name} (#{coin}) 24 | //- else 25 | //- span #{coinConfigs[coin].name} (#{coin}) 26 | 27 | //- br 28 | //- span #{config.donationAddresses[coin].address} 29 | //- br 30 | //- img(src=donationAddressQrCodeUrls[coin], alt=config.donationAddresses[coin].address, style="border: solid 1px #ccc;") 31 | //- br 32 | -------------------------------------------------------------------------------- /views/block.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headContent 4 | title Block ##{result.getblock.height.toLocaleString()}, #{result.getblock.hash} 5 | 6 | block content 7 | h1(class="h3") Block 8 | small(style="width: 100%;", class="monospace") ##{result.getblock.height.toLocaleString()} 9 | br 10 | small(style="width: 100%;", class="monospace word-wrap") #{result.getblock.hash} 11 | hr 12 | 13 | include includes/block-content.pug -------------------------------------------------------------------------------- /views/blocks.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headContent 4 | title Blocks 5 | 6 | block content 7 | h1(class="h2") Blocks 8 | hr 9 | 10 | if (blocks) 11 | nav(aria-label="Page navigation") 12 | ul(class="pagination justify-content-center") 13 | 14 | li(class="page-item", class=(sort == "desc" ? "active" : false)) 15 | a(class="page-link", href=(sort == "desc" ? "javascript:void(0)" : "/blocks?limit=" + limit + "&offset=0" + "&sort=desc")) 16 | span(aria-hidden="true") Newest blocks first 17 | 18 | li(class="page-item", class=(sort == "asc" ? "active" : false)) 19 | a(class="page-link", href=(sort == "asc" ? "javascript:void(0)" : "/blocks?limit=" + limit + "&offset=0" + "&sort=asc")) 20 | span(aria-hidden="true") Oldest blocks first 21 | 22 | include includes/blocks-list.pug 23 | 24 | if (blockCount > limit) 25 | - var pageNumber = offset / limit + 1; 26 | - var pageCount = Math.floor(blockCount / limit); 27 | - if (pageCount * limit < blockCount) { 28 | - pageCount++; 29 | - } 30 | - var paginationUrlFunction = function(x) { 31 | - return paginationBaseUrl + "?limit=" + limit + "&offset=" + ((x - 1) * limit + "&sort=" + sort); 32 | - } 33 | 34 | hr 35 | 36 | include includes/pagination.pug 37 | else 38 | p No blocks found -------------------------------------------------------------------------------- /views/browser.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headContent 4 | title RPC Browser #{(method ? (" - " + method) : false)} 5 | 6 | style. 7 | pre { 8 | white-space: pre-wrap; /* Since CSS 2.1 */ 9 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 10 | } 11 | 12 | block content 13 | h1(class="h3") RPC Browser 14 | hr 15 | 16 | if (gethelp) 17 | div(class="row") 18 | div(class="col-md-9") 19 | if (methodhelp) 20 | div(class="row") 21 | div(class="col") 22 | h4(style="display: inline-block;") 23 | span(class="text-muted") Command: 24 | span #{method} 25 | div(class="col") 26 | a(href=("https://bitcoin.org/en/developer-reference#" + method), class="btn btn-primary float-md-right") See developer docs » 27 | 28 | 29 | hr 30 | 31 | ul(class='nav nav-tabs mb-3') 32 | li(class="nav-item") 33 | a(data-toggle="tab", href="#tab-execute", class="nav-link active", role="tab") Execute 34 | li(class="nav-item") 35 | a(data-toggle="tab", href="#tab-help-content", class="nav-link", role="tab") Help Content 36 | 37 | div(class="tab-content") 38 | div(id="tab-execute", class="tab-pane active pb-3", role="tabpanel") 39 | if (methodResult) 40 | div(class="mt-4") 41 | h5(class="mt-3") Result 42 | 43 | pre(style="border: solid 1px #ccc;") 44 | code #{JSON.stringify(methodResult, null, 4)} 45 | 46 | hr 47 | 48 | form(method="get") 49 | input(type="hidden", name="method", value=method) 50 | 51 | div(class="h5 mb-3") Arguments 52 | 53 | div(class="ml-3") 54 | each argX, index in methodhelp.args 55 | div(class="form-group") 56 | label(for=("arg_" + argX.name)) 57 | strong #{argX.name} 58 | span (#{argX.properties.join(", ")}) 59 | if (argX.description) 60 | span - #{argX.description} 61 | if (false && argX.detailsLines && argX.detailsLines.length > 0) 62 | - var detailsLines = ""; 63 | each detailsLine in argX.detailsLines 64 | - detailsLines = (detailsLines + "
" + detailsLine); 65 | i(class="fas fa-info-circle", data-toggle="tooltip", title=detailsLines) 66 | 67 | 68 | 69 | - var valX = false; 70 | if (argValues != null) 71 | if (argValues[index] != null) 72 | if (("" + argValues[index]) != NaN) 73 | - valX = argValues[index].toString(); 74 | 75 | input(id=("arg_" + argX.name), type="text", name=("args[" + index + "]"), placeholder=argX.name, class="form-control", value=valX) 76 | 77 | if (!methodhelp.args || methodhelp.args.length == 0) 78 | span(class="text-muted") None 79 | 80 | hr 81 | 82 | input(type="submit", name="execute", value="Execute", class="btn btn-primary btn-block") 83 | 84 | div(id="tab-help-content", class="tab-pane", role="tabpanel") 85 | pre #{methodhelp.string} 86 | 87 | 88 | 89 | 90 | else 91 | :markdown-it 92 | Browse RPC commands from the list. The list is built from the results of the `help` command and organized into sections accordingly. 93 | 94 | div(class="col-md-3") 95 | each section, sectionIndex in gethelp 96 | h4 #{section.name} 97 | small (#{section.methods.length}) 98 | hr 99 | 100 | div(class="mb-4") 101 | ol(style="padding-left: 30px;") 102 | each methodX, methodIndex in section.methods 103 | li 104 | a(href=("/rpc-browser?method=" + methodX.name), style=(methodX.name == method ? "font-weight: bold; font-style: italic;" : false), class="monospace") #{methodX.name} 105 | 106 | -------------------------------------------------------------------------------- /views/connect.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1(class="h2") RPC Connect 5 | hr 6 | 7 | form(method="post", action="/connect") 8 | div(class="form-group") 9 | label(for="input-host") Host / IP 10 | input(id="input-host", type="text", name="host", class="form-control", placeholder="Host / IP", value=host) 11 | 12 | div(class="form-group") 13 | label(for="input-port") Port 14 | input(id="input-port", type="text", name="port", class="form-control", placeholder="Port", value=port) 15 | 16 | div(class="form-group") 17 | label(for="input-username") Username 18 | input(id="input-username", type="text", name="username", class="form-control", placeholder="Username", value=username) 19 | 20 | div(class="form-group") 21 | label(for="input-password") Password 22 | input(id="input-password", type="password", name="password", class="form-control", placeholder="Password") 23 | 24 | div(class="form-group") 25 | input(type="submit", class="btn btn-primary btn-block" value="Connect") 26 | -------------------------------------------------------------------------------- /views/error.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1 Error 5 | hr 6 | 7 | if (message) 8 | p !{message} 9 | else 10 | p Unknown error 11 | 12 | if (error) 13 | h2 #{error.status} 14 | pre #{error.stack} 15 | -------------------------------------------------------------------------------- /views/fun.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headContent 4 | title #{coinConfig.name} Fun 5 | 6 | block content 7 | h1(class="h3") #{coinConfig.name} Fun 8 | hr 9 | 10 | p Below is a list of fun/interesting things in the #{coinConfig.name} blockchain. Some are historical firsts, others are just fun or cool. 11 | 12 | table(class="table table-striped table-responsive-sm mt-4") 13 | thead 14 | tr 15 | th(class="data-header") Date 16 | th(class="data-header") Description 17 | th(class="data-header") Link 18 | th(class="data-header") Reference 19 | tbody 20 | each item, index in coinConfig.historicalData 21 | tr 22 | td(class="data-cell") #{item.date} 23 | 24 | td(class="data-cell") #{item.summary} 25 | 26 | td(class="data-cell monospace") 27 | if (item.type == "tx") 28 | a(href=("/tx/" + item.txid), title=item.txid, data-toggle="tooltip") Tx #{item.txid.substring(0, 23)}... 29 | else if (item.type == "block") 30 | a(href=("/block/" + item.blockHash), title="Block #{item.blockHash}", data-toggle="tooltip") Block #{item.blockHash.substring(0, 20)}... 31 | else if (item.type == "blockheight") 32 | a(href=("/block/" + item.blockHash)) Block ##{item.blockHeight} 33 | else if (item.type == "address") 34 | a(href=("/address/" + item.address), title=item.address, data-toggle="tooltip") Address #{item.address.substring(0, 18)}... 35 | else if (item.type == "link") 36 | a(href=item.url) #{item.url.substring(0, 20)}... 37 | 38 | td(class="data-cell") 39 | if (item.referenceUrl && item.referenceUrl.trim().length > 0) 40 | - var matches = item.referenceUrl.match(/^https?\:\/\/([^\/:?#]+)(?:[\/:?#]|$)/i); 41 | 42 | - var domain = null; 43 | - var domain = matches && matches[1]; 44 | 45 | if (domain) 46 | a(href=item.referenceUrl, rel="nofollow") #{domain} 47 | i(class="fas fa-external-link-alt") 48 | else 49 | a(href=item.referenceUrl, rel="nofollow") Reference 50 | else 51 | span - -------------------------------------------------------------------------------- /views/includes/block-content.pug: -------------------------------------------------------------------------------- 1 | - var txCount = result.getblock.txCount; 2 | 3 | ul(class='nav nav-tabs mb-3') 4 | li(class="nav-item") 5 | a(data-toggle="tab", href="#tab-details", class="nav-link active", role="tab") Details 6 | if(txCount < 1000) 7 | li(class="nav-item") 8 | a(data-toggle="tab", href="#tab-json", class="nav-link", role="tab") JSON 9 | 10 | 11 | div(class="tab-content") 12 | div(id="tab-details", class="tab-pane active", role="tabpanel") 13 | if (global.specialBlocks && global.specialBlocks[result.getblock.hash]) 14 | div(class="alert alert-primary", style="padding-bottom: 0;") 15 | div(class="float-left", style="width: 55px; height: 55px; font-size: 18px;") 16 | i(class="fas fa-certificate fa-2x", style="margin-top: 10px;") 17 | h4(class="alert-heading h6 font-weight-bold") #{coinConfig.name} Fun 18 | 19 | // special transaction info 20 | - var sbInfo = global.specialBlocks[result.getblock.hash]; 21 | if (sbInfo.alertBodyHtml) 22 | p 23 | span !{sbInfo.alertBodyHtml} 24 | 25 | if (sbInfo.referenceUrl && sbInfo.referenceUrl.trim().length > 0 && sbInfo.alertBodyHtml.indexOf(sbInfo.referenceUrl) == -1) 26 | span 27 | a(href=sbInfo.referenceUrl) Read more 28 | 29 | else 30 | p 31 | span #{sbInfo.summary} 32 | 33 | if (sbInfo.referenceUrl && sbInfo.referenceUrl.trim().length > 0) 34 | span 35 | a(href=sbInfo.referenceUrl) Read more 36 | 37 | div(class="card mb-3") 38 | div(class="card-header") 39 | span(class="h6") Summary 40 | div(class="card-body") 41 | div(class="row") 42 | div(class="col-md-6") 43 | div(class="row") 44 | div(class="summary-split-table-label") Previous Block 45 | div(class="summary-split-table-content monospace") 46 | if (result.getblock.previousblockhash) 47 | a(class="word-wrap", href=("/block/" + result.getblock.previousblockhash)) #{result.getblock.previousblockhash} 48 | br 49 | span (##{(result.getblock.height - 1).toLocaleString()}) 50 | 51 | else if (result.getblock.hash == genesisBlockHash) 52 | span None (genesis block) 53 | 54 | div(class="row") 55 | div(class="summary-split-table-label") Timestamp 56 | div(class="summary-split-table-content monospace") 57 | span #{moment.utc(new Date(result.getblock.time * 1000)).format("Y-MM-DD HH:mm:ss")} utc 58 | - var timeAgoTime = result.getblock.time; 59 | include ./time-ago.pug 60 | 61 | div(class="row") 62 | div(class="summary-split-table-label") Transactions 63 | div(class="summary-split-table-content monospace") #{result.getblock.txCount.toLocaleString()} 64 | 65 | div(class="row") 66 | div(class="summary-split-table-label") Total Fees 67 | div(class="summary-split-table-content monospace") 68 | - var currencyValue = new Decimal(result.getblock.totalFees); 69 | include ./value-display.pug 70 | 71 | if (result.getblock.totalFees > 0) 72 | div(class="row") 73 | div(class="summary-split-table-label") Average Fee 74 | div(class="summary-split-table-content monospace") 75 | - var currencyValue = new Decimal(result.getblock.totalFees).dividedBy(result.getblock.txCount); 76 | include ./value-display.pug 77 | 78 | - var blockRewardMax = coinConfig.blockRewardFunction(result.getblock.height); 79 | - var coinbaseTxTotalOutputValue = new Decimal(0); 80 | each vout in result.getblock.coinbaseTx.vout 81 | - coinbaseTxTotalOutputValue = coinbaseTxTotalOutputValue.plus(new Decimal(vout.value)); 82 | 83 | if (coinbaseTxTotalOutputValue < blockRewardMax) 84 | div(class="row") 85 | div(class="summary-split-table-label") Fees Destroyed 86 | div(class="summary-split-table-content monospace text-danger") 87 | - var currencyValue = new Decimal(blockRewardMax).minus(coinbaseTxTotalOutputValue); 88 | include ./value-display.pug 89 | 90 | a(class="ml-2", data-toggle="tooltip", title="The miner of this block failed to collect this value. As a result, it is lost.") 91 | i(class="fas fa-info-circle") 92 | 93 | if (result.getblock.weight) 94 | div(class="row") 95 | div(class="summary-split-table-label") Weight 96 | div(class="summary-split-table-content monospace") 97 | span(style="") #{result.getblock.weight.toLocaleString()} wu 98 | 99 | - var radialProgressBarPercent = new Decimal(100 * result.getblock.weight / coinConfig.maxBlockWeight).toDecimalPlaces(2); 100 | include ./radial-progress-bar.pug 101 | 102 | span(class="text-muted") (#{new Decimal(100 * result.getblock.weight / coinConfig.maxBlockWeight).toDecimalPlaces(2)}% full) 103 | 104 | div(class="row") 105 | div(class="summary-split-table-label") Size 106 | div(class="summary-split-table-content monospace") #{result.getblock.size.toLocaleString()} bytes 107 | 108 | div(class="row") 109 | div(class="summary-split-table-label") Confirmations 110 | div(class="summary-split-table-content monospace") 111 | if (result.getblock.confirmations < 6) 112 | span(class="font-weight-bold text-danger") #{result.getblock.confirmations.toLocaleString()} 113 | a(data-toggle="tooltip", title="< 6 confirmations is generally considered too few for high-value transactions. This convention's applicability may vary.") 114 | i(class="fas fa-unlock-alt") 115 | else 116 | span(class="font-weight-bold text-success font-weight-bold") #{result.getblock.confirmations.toLocaleString()} 117 | a(data-toggle="tooltip", title="6 confirmations is generally considered 'irreversible'. High-value transactions may require more, low-value transactions may require less.") 118 | i(class="fas fa-lock") 119 | 120 | 121 | div(class="col-md-6") 122 | div(class="row") 123 | div(class="summary-split-table-label") Next Block 124 | div(class="summary-split-table-content monospace") 125 | if (result.getblock.nextblockhash) 126 | a(href=("/block/" + result.getblock.nextblockhash)) #{result.getblock.nextblockhash} 127 | br 128 | span (##{(result.getblock.height + 1).toLocaleString()}) 129 | else 130 | span None (latest block) 131 | 132 | div(class="row") 133 | div(class="summary-split-table-label") Difficulty 134 | div(class="summary-split-table-content monospace") 135 | - var difficultyData = utils.formatLargeNumber(result.getblock.difficulty, 3); 136 | 137 | span(title=parseFloat(result.getblock.difficulty).toLocaleString(), data-toggle="tooltip") 138 | span #{difficultyData[0]} 139 | span x 10 140 | sup #{difficultyData[1].exponent} 141 | 142 | div(class="row") 143 | div(class="summary-split-table-label") Version 144 | div(class="summary-split-table-content monospace") 0x#{result.getblock.versionHex} 145 | span(class="text-muted") (decimal: #{result.getblock.version}) 146 | 147 | div(class="row") 148 | div(class="summary-split-table-label") Nonce 149 | div(class="summary-split-table-content monospace") #{result.getblock.nonce} 150 | 151 | div(class="row") 152 | div(class="summary-split-table-label") Bits 153 | div(class="summary-split-table-content monospace") #{result.getblock.bits} 154 | 155 | div(class="row") 156 | div(class="summary-split-table-label") Merkle Root 157 | div(class="summary-split-table-content monospace") #{result.getblock.merkleroot} 158 | 159 | div(class="row") 160 | div(class="summary-split-table-label") Chainwork 161 | div(class="summary-split-table-content monospace") 162 | - var chainworkData = utils.formatLargeNumber(parseInt("0x" + result.getblock.chainwork), 2); 163 | 164 | span #{chainworkData[0]} 165 | span x 10 166 | sup #{chainworkData[1].exponent} 167 | span hashes 168 | 169 | span(class="text-muted") (#{result.getblock.chainwork.replace(/^0+/, '')}) 170 | 171 | div(class="row") 172 | div(class="summary-split-table-label") Miner 173 | div(class="summary-split-table-content monospace") 174 | if (result.getblock.miner) 175 | if(result.getblock.miner.name) 176 | span #{result.getblock.miner.name} 177 | else 178 | span #{result.getblock.miner} 179 | if (result.getblock.miner.identifiedBy) 180 | span(data-toggle="tooltip", title=("Identified by: " + result.getblock.miner.identifiedBy)) 181 | i(class="fas fa-info-circle") 182 | else 183 | span ? 184 | span(data-toggle="tooltip", title="Unable to identify miner") 185 | i(class="fas fa-info-circle") 186 | 187 | 188 | div(class="card mb-3") 189 | div(class="card-header") 190 | div(class="row") 191 | div(class="col-md-4") 192 | h2(class="h6 mb-0") #{txCount.toLocaleString()} 193 | if (txCount == 1) 194 | span Transaction 195 | else 196 | span Transactions 197 | 198 | //- if (txCount > 10) 199 | //- div(class="col-md-8 text-right") 200 | //- span(class="mr-2") Show 201 | //- div(class="btn-group", role="group") 202 | //- a(href=(paginationBaseUrl + "?limit=10"), class="btn btn-sm btn-primary px-2", class=((limit == 10 && txCount > limit) ? "active" : false)) 10 203 | 204 | //- if (txCount > 50) 205 | //- a(href=(paginationBaseUrl + "?limit=50"), class="btn btn-sm btn-primary px-2", class=(limit == 50 ? "active" : false)) 50 206 | 207 | //- if (txCount > 100) 208 | //- a(href=(paginationBaseUrl + "?limit=100"), class="btn btn-sm btn-primary px-2", class=(limit == 100 ? "active" : false)) 100 209 | 210 | //- if (txCount < 1000) 211 | //- a(href=(paginationBaseUrl + "?limit=3000"), class="btn btn-sm btn-primary px-2", class=(limit >= txCount ? "active" : false)) all 212 | 213 | - var fontawesomeInputName = "sign-in-alt"; 214 | - var fontawesomeOutputName = "sign-out-alt"; 215 | 216 | div(class="card-body") 217 | each tx, txIndex in result.transactions 218 | //pre 219 | // code #{JSON.stringify(tx, null, 4)} 220 | div(class="xcard mb-3") 221 | div(class="card-header monospace") 222 | if (tx && tx.txid) 223 | span ##{(txIndex + offset + 1).toLocaleString()} 224 | span – 225 | a(href=("/tx/" + tx.txid)) #{tx.txid} 226 | if (global.specialTransactions && global.specialTransactions[tx.txid]) 227 | span 228 | a(data-toggle="tooltip", title=(coinConfig.name + " Fun! See transaction for details")) 229 | i(class="fas fa-certificate text-primary") 230 | span 231 | a(href=("#" + tx.txid) data-toggle="collapse" data-parent="#accordion" aria-expanded="false" aria-controls=("#" + tx.txid) style="padding-right: inherit;") 232 | i(class="fa fa-chevron-down pull-right") 233 | 234 | if(tx.vout) 235 | each vout, voutIndex in tx.vout 236 | if (vout && vout.scriptPubKey && vout.scriptPubKey.asm && vout.scriptPubKey.asm.startsWith('OP_RETURN ')) 237 | - var opReturn = utils.getOpReturnTags(vout.scriptPubKey.asm); 238 | if(opReturn.tag == "memo.cash") 239 | span(class="tag-memo") #{opReturn.tag} 240 | span(class="tag-memo") #{opReturn.action} 241 | else if (opReturn.tag == "yours.org") 242 | span(class="tag-yours") #{opReturn.tag} 243 | else 244 | span(class="tag") "OP_RETURN" 245 | 246 | div(class="card-body collapse " role="tabpanel" id=tx.txid) 247 | //pre 248 | // code #{JSON.stringify(result.txInputsByTransaction[tx.txid], null, 4)} 249 | - var txInputs = result.txInputsByTransaction[tx.txid]; 250 | - var blockHeight = result.getblock.height; 251 | 252 | include ./transaction-io-details.pug 253 | 254 | 255 | //pre 256 | // code #{JSON.stringify(tx, null, 4)} 257 | 258 | if (!crawlerBot && txCount > limit) 259 | - var pageNumber = offset / limit + 1; 260 | - var pageCount = Math.floor(txCount / limit); 261 | - if (pageCount * limit < txCount) { 262 | - pageCount++; 263 | - } 264 | - var paginationUrlFunction = function(x) { 265 | - return paginationBaseUrl + "?limit=" + limit + "&offset=" + ((x - 1) * limit); 266 | - } 267 | 268 | hr 269 | 270 | include ./pagination.pug 271 | 272 | if(txCount < 1000) 273 | div(id="tab-json", class="tab-pane", role="tabpanel") 274 | pre 275 | code #{JSON.stringify(result.getblock, null, 4)} 276 | 277 | -------------------------------------------------------------------------------- /views/includes/blocks-list.pug: -------------------------------------------------------------------------------- 1 | table(class="table table-striped table-responsive-sm mb-0") 2 | thead 3 | tr 4 | //th 5 | th(class="data-header") Height 6 | th(class="data-header") Timestamp (utc) 7 | th(class="data-header text-right") Age 8 | th(class="data-header") Miner 9 | th(class="data-header text-right") Transactions 10 | th(class="data-header text-right") Average Fee 11 | th(class="data-header text-right") Size (bytes) 12 | 13 | if (blocks && blocks.length > 0 && blocks[0].weight) 14 | th(class="data-header text-right") Weight (wu) 15 | tbody 16 | each block, blockIndex in blocks 17 | if (block) 18 | tr 19 | td(class="data-cell monospace") 20 | a(href=("/block-height/" + block.height)) #{block.height.toLocaleString()} 21 | 22 | if (global.specialBlocks && global.specialBlocks[block.hash]) 23 | span 24 | a(data-toggle="tooltip", title=(coinConfig.name + " Fun! See block for details")) 25 | i(class="fas fa-certificate text-primary") 26 | td(class="data-cell monospace") #{moment.utc(new Date(parseInt(block.time) * 1000)).format("Y-MM-DD HH:mm:ss")} 27 | 28 | - var timeAgo = moment.duration(moment.utc(new Date()).diff(moment.utc(new Date(parseInt(block.time) * 1000)))); 29 | td(class="data-cell monospace text-right") #{timeAgo.format()} 30 | td(class="data-cell monospace") 31 | if (block.miner && block.miner.name) 32 | span(data-toggle="tooltip", title=("Identified by: " + block.miner.identifiedBy), class="tag") #{block.miner.name} 33 | else if (block.miner) 34 | span(data-toggle="tooltip", title=("Coinbase: " + block.miner), class="tag") #{block.miner} 35 | else 36 | span ? 37 | 38 | 39 | td(class="data-cell monospace text-right") #{block.tx.length.toLocaleString()} 40 | 41 | td(class="data-cell monospace text-right") 42 | - var currencyValue = new Decimal(block.totalFees).dividedBy(block.tx.length); 43 | include ./value-display.pug 44 | 45 | td(class="data-cell monospace text-right") #{block.size.toLocaleString()} 46 | 47 | if (blocks && blocks.length > 0 && blocks[0].weight) 48 | td(class="data-cell monospace text-right") 49 | span #{block.weight.toLocaleString()} 50 | 51 | - var radialProgressBarPercent = new Decimal(100 * block.weight / coinConfig.maxBlockWeight).toDecimalPlaces(2); 52 | include ./radial-progress-bar.pug -------------------------------------------------------------------------------- /views/includes/electrum-trust-note.pug: -------------------------------------------------------------------------------- 1 | span 2 | span(data-toggle="tooltip", title="This data is at least partially generated from the ElectrumX servers currently configured: ") 3 | i(class="fas fa-exclamation-triangle text-warning") -------------------------------------------------------------------------------- /views/includes/graph.pug: -------------------------------------------------------------------------------- 1 | 2 | div(id='viz') 3 | script(type="text/javascript"). 4 | 5 | var blocks = !{JSON.stringify(latestBlocks)} 6 | 7 | // Create the input graph 8 | var g = new dagreD3.graphlib.Graph() 9 | .setGraph({}) 10 | .setDefaultEdgeLabel(function() { return {}; }); 11 | g.graph().rankDir = 'RL'; 12 | 13 | for(var i=0;i<=blocks.length-3;i++) { 14 | if(blocks[i].miner && blocks[i].miner.name){ 15 | g.setNode(i, { label: '#'+blocks[i].height + '\n' + blocks[i].miner.name, class: "type-S" }); 16 | } 17 | else if (blocks[i].miner){ 18 | g.setNode(i, { label: '#'+blocks[i].height + '\n' + blocks[i].miner.substring(0, 3)+"...", class: "type-S" }); 19 | } 20 | else{ 21 | g.setNode(i, { label: '#'+blocks[i].height + '\n' + 'Unknown', class: "type-S" }); 22 | } 23 | } 24 | 25 | g.setNode(999, {label: '', shape:"img" }); //Alpha node 26 | 27 | g.nodes().forEach(function(v) { 28 | var node = g.node(v); 29 | // Round the corners of the nodes 30 | node.rx = node.ry = 5; 31 | 32 | if(node.id === 999){ 33 | node.shape = "img"; 34 | } 35 | 36 | }); 37 | 38 | 39 | 40 | for(var i=0 ;i= (pageNumber - 4) && x <= (pageNumber + 4) || xIndex == 0 || xIndex == (pageNumbers.length - 1)) 13 | li(class="page-item", class=(x == pageNumber ? "active" : false)) 14 | a(class="page-link", href=(paginationUrlFunction(x))) #{x} 15 | 16 | if (x == 1 && pageNumber > 6) 17 | li(class="page-item disabled") 18 | a(class="page-link", href="javascript:void(0)") ... 19 | 20 | else if (x == (pageCount - 1) && pageNumber < (pageCount - 5)) 21 | li(class="page-item disabled") 22 | a(class="page-link", href="javascript:void(0)") ... 23 | 24 | li(class="page-item", class=(pageNumber == pageCount ? "disabled" : false)) 25 | a(class="page-link", href=(pageNumber == pageCount ? "javascript:void(0)" : paginationUrlFunction(pageNumber + 1)), aria-label="Next") 26 | span(aria-hidden="true") » -------------------------------------------------------------------------------- /views/includes/radial-progress-bar.pug: -------------------------------------------------------------------------------- 1 | div(class="radial-progress", data-progress=parseInt(radialProgressBarPercent), data-toggle="tooltip", title=(radialProgressBarPercent + "% full")) 2 | div(class="circle") 3 | div(class="mask full") 4 | div(class="fill") 5 | div(class="mask half") 6 | div(class="fill") 7 | div(class="fill fix") 8 | div(class="inset") -------------------------------------------------------------------------------- /views/includes/time-ago.pug: -------------------------------------------------------------------------------- 1 | - var timeAgo = moment.duration(moment.utc(new Date()).diff(moment.utc(new Date(parseInt(timeAgoTime) * 1000)))); 2 | 3 | span 4 | span(data-toggle="tooltip", title=(timeAgo.format() + " ago")) 5 | i(class="far fa-clock") -------------------------------------------------------------------------------- /views/includes/tools-card.pug: -------------------------------------------------------------------------------- 1 | div(class="card mb-3") 2 | div(class="card-header") 3 | h2(class="h6 mb-0") Tools 4 | div(class="card-body") 5 | div(class="row") 6 | each item, index in [[0, 1, 2], [4, 3, 5] , [6]] 7 | div(class="col-md-4") 8 | ul(style="list-style-type: none;", class="pl-0") 9 | each toolIndex, toolIndexIndex in item 10 | - var siteTool = config.siteTools[toolIndex]; 11 | 12 | li 13 | div(class="float-left", style="height: 50px; width: 40px; margin-right: 10px;") 14 | span 15 | i(class=siteTool.fontawesome, class="fa-2x mr-2", style="margin-top: 6px;") 16 | 17 | a(href=siteTool.url) #{siteTool.name} 18 | br 19 | p #{siteTool.desc} -------------------------------------------------------------------------------- /views/includes/transaction-io-details.pug: -------------------------------------------------------------------------------- 1 | - var fontawesomeInputName = "sign-in-alt"; 2 | - var fontawesomeOutputName = "sign-out-alt"; 3 | 4 | - var totalIOValues = utils.getTxTotalInputOutputValues(tx, txInputs, blockHeight); 5 | 6 | div(class="row monospace") 7 | div(class="col-lg-6") 8 | if (txInputs) 9 | - var extraInputCount = 0; 10 | each txVin, txVinIndex in tx.vin 11 | if (!txVin.coinbase) 12 | - var vout = null; 13 | if (txInputs && txInputs[txVinIndex]) 14 | - var txInput = txInputs[txVinIndex]; 15 | if (txInput.vout && txInput.vout[txVin.vout]) 16 | - var vout = txInput.vout[txVin.vout]; 17 | 18 | if (txVin.coinbase || vout) 19 | div(class="row") 20 | div(class="tx-io-label") 21 | a(data-toggle="tooltip", title=("Input #" + (txVinIndex + 1).toLocaleString()), style="white-space: nowrap;") 22 | i(class=("fas fa-" + fontawesomeInputName + " mr-2")) 23 | span(class="d-inline d-md-none") Input # 24 | span #{(txVinIndex + 1).toLocaleString()} 25 | 26 | div(class="tx-io-content") 27 | div(class="row") 28 | div(class="tx-io-desc") 29 | if (txVin.coinbase) 30 | span(class="tag") coinbase 31 | span Newly minted coins 32 | else 33 | if (vout && vout.scriptPubKey && vout.scriptPubKey.addresses) 34 | div(style="word-break: break-word;") 35 | a(href=("/address/" + vout.scriptPubKey.addresses[0])) #{vout.scriptPubKey.addresses[0]} 36 | if (global.specialAddresses[vout.scriptPubKey.addresses[0]]) 37 | - var specialAddressInfo = global.specialAddresses[vout.scriptPubKey.addresses[0]]; 38 | if (specialAddressInfo.type == "minerPayout") 39 | span 40 | a(data-toggle="tooltip", title=("Miner payout address: " + specialAddressInfo.minerInfo.name)) 41 | i(class="fas fa-certificate text-primary") 42 | else if (specialAddressInfo.type == "donation") 43 | span 44 | a(data-toggle="tooltip", title=("Development donation address. All support is appreciated!")) 45 | i(class="fas fa-certificate text-primary") 46 | 47 | span(class="small") via 48 | a(href=("/tx/" + txInput.txid + "#output-" + txVin.vout)) #{txInput.txid.substring(0, 20)}...[#{txVin.vout}] 49 | 50 | div(class="tx-io-value") 51 | if (txVin.coinbase) 52 | - var currencyValue = coinConfig.blockRewardFunction(blockHeight); 53 | include ./value-display.pug 54 | else 55 | if (vout && vout.value) 56 | - var currencyValue = vout.value; 57 | include ./value-display.pug 58 | 59 | hr 60 | 61 | else 62 | - extraInputCount = extraInputCount + 1; 63 | 64 | div(class="row mb-5") 65 | div(class="col") 66 | div(class="font-weight-bold text-left text-md-right") 67 | span(class="d-inline d-md-none") Total Input: 68 | - var currencyValue = totalIOValues.input; 69 | include ./value-display.pug 70 | 71 | 72 | 73 | 74 | div(class="col-lg-6") 75 | each vout, voutIndex in tx.vout 76 | div(class="row") 77 | div(class="tx-io-label") 78 | a(data-toggle="tooltip", title=("Output #" + (voutIndex + 1).toLocaleString()), style="white-space: nowrap;") 79 | i(class=("fas fa-" + fontawesomeInputName + " mr-2")) 80 | span(class="d-inline d-md-none") Output # 81 | span #{(voutIndex + 1).toLocaleString()} 82 | 83 | div(class="tx-io-content") 84 | div(class="row") 85 | div(class="tx-io-desc") 86 | if (vout.scriptPubKey) 87 | if (vout.scriptPubKey.addresses) 88 | a(id=("output-" + voutIndex), href=("/address/" + vout.scriptPubKey.addresses[0])) 89 | span(class="monospace", style="word-break: break-word;") #{vout.scriptPubKey.addresses[0]} 90 | 91 | if (global.specialAddresses[vout.scriptPubKey.addresses[0]]) 92 | - var specialAddressInfo = global.specialAddresses[vout.scriptPubKey.addresses[0]]; 93 | if (specialAddressInfo.type == "minerPayout") 94 | span 95 | a(data-toggle="tooltip", title=("Miner payout address: " + specialAddressInfo.minerInfo.name)) 96 | i(class="fas fa-certificate text-primary") 97 | else if (specialAddressInfo.type == "donation") 98 | span 99 | a(data-toggle="tooltip", title=("Development donation address. All support is appreciated!")) 100 | i(class="fas fa-certificate text-primary") 101 | 102 | else if (vout.scriptPubKey.hex && vout.scriptPubKey.hex.startsWith('6a24aa21a9ed')) 103 | span(class="monospace") Segregated Witness committment 104 | a(href="https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#commitment-structure", data-toggle="tooltip", title="View developer docs", target="_blank") 105 | i(class="fas fa-info-circle") 106 | 107 | else if (vout.scriptPubKey.asm && vout.scriptPubKey.asm.startsWith('OP_RETURN ')) 108 | span(class="monospace") OP_RETURN: 109 | 110 | - var opReturn = utils.getOpReturnTags(vout.scriptPubKey.asm); 111 | if(opReturn && opReturn.tag) 112 | span(class="monospace") #{vout.scriptPubKey.asm.substring("OP_RETURN ".length)} 113 | 114 | br 115 | span(class="monospace text-muted") ASCII: #{utils.hex2ascii(vout.scriptPubKey.asm.substring("OP_RETURN ".length))} 116 | 117 | else 118 | span(class="monospace") 119 | span(class="text-danger font-weight-bold") Unable to decode output: 120 | br 121 | span(class="font-weight-bold") type: 122 | span #{vout.scriptPubKey.type} 123 | br 124 | span(class="font-weight-bold") asm: 125 | span #{vout.scriptPubKey.asm} 126 | br 127 | span(class="font-weight-bold") decodedHex: 128 | span #{utils.hex2ascii(vout.scriptPubKey.hex)} 129 | 130 | div(class="tx-io-value") 131 | - var currencyValue = vout.value; 132 | include ./value-display.pug 133 | 134 | hr 135 | 136 | div(class="row mb-5") 137 | div(class="col") 138 | div(class="font-weight-bold text-left text-md-right") 139 | span(class="d-inline d-md-none") Total Output: 140 | - var currencyValue = totalIOValues.output; 141 | include ./value-display.pug 142 | 143 | 144 | -------------------------------------------------------------------------------- /views/includes/value-display.pug: -------------------------------------------------------------------------------- 1 | if (currencyValue > 0) 2 | span(class="monospace") #{utils.formatCurrencyAmount(currencyValue, currencyFormatType)} 3 | if (global.exchangeRate) 4 | span 5 | span(data-toggle="tooltip", title=utils.formatExchangedCurrency(currencyValue)) 6 | i(class="fas fa-exchange-alt") 7 | else 8 | span(class="monospace") 0 -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headContent 4 | title Home 5 | 6 | block content 7 | h1(class="h3") #{coinConfig.pageTitle} 8 | span(class="tag" style="font-size: small" ) Auto refreshed every 30 secs 9 | hr 10 | include includes/graph.pug 11 | 12 | //- if (config.demoSite && session.hideHomepageBanner != "true") 13 | //- div(class="alert alert-primary alert-dismissible", role="alert") 14 | //- p 15 | //- strong #{coinConfig.siteTitle} 16 | //- span is 17 | //- a(href="https://github.com/janoside/btc-rpc-explorer") open-source 18 | //- span and easy to set up. It can communicate with your 19 | //- a(href=coinConfig.nodeUrl) #{coinConfig.name} Full Node 20 | //- span via RPC. See the 21 | //- a(href="https://github.com/janoside/btc-rpc-explorer") project description 22 | //- span for a list of features and instructions for running. 23 | 24 | //- div(style="height: 34px;") 25 | //- a(class="github-button", href="https://github.com/janoside/btc-rpc-explorer", data-icon="octicon-star", data-size="large", data-show-count="true", aria-label="Star janoside/btc-rpc-explorer on GitHub", style="padding-right: 10px;") Star 26 | 27 | //- span 28 | //- a(class="github-button", href="https://github.com/janoside/btc-rpc-explorer/fork", data-icon="octicon-repo-forked", data-size="large", data-show-count="true", aria-label="Fork janoside/btc-rpc-explorer on GitHub") Fork 29 | 30 | //- a(href="/changeSetting?name=hideHomepageBanner&value=true", class="close", aria-label="Close", style="text-decoration: none;") 31 | //- span(aria-hidden="true") × 32 | 33 | div(class="card mb-3") 34 | div(class="card-header") 35 | h2(class="h6 mb-0") Network Summary 36 | div(class="card-body") 37 | div(class="row") 38 | div(class="col-md-4") 39 | ul(style="list-style-type: none;", class="pl-0") 40 | li 41 | div(class="float-left", style="height: 40px; width: 40px;") 42 | span 43 | i(class="fas fa-tachometer-alt fa-2x mr-2", style="margin-top: 6px;") 44 | - var hashrateData = utils.formatLargeNumber(miningInfo.networkhashps, 3); 45 | 46 | span(class="font-weight-bold") Hashrate 47 | 48 | p(class="lead") 49 | span #{hashrateData[0]} 50 | span(title=(hashrateData[1].name + "-hash / x10^" + hashrateData[1].exponent), data-toggle="tooltip") #{hashrateData[1].abbreviation}H/s 51 | 52 | if (getblockchaininfo.size_on_disk) 53 | li 54 | div(class="float-left", style="height: 40px; width: 40px;") 55 | span 56 | i(class="fas fa-database fa-2x mr-2", style="margin-top: 6px; margin-left: 3px;") 57 | span(class="font-weight-bold") Blockchain Size 58 | 59 | - var sizeData = utils.formatLargeNumber(getblockchaininfo.size_on_disk, 2); 60 | p(class="lead") #{sizeData[0]} #{sizeData[1].abbreviation}B 61 | 62 | div(class="col-md-4") 63 | ul(style="list-style-type: none;", class="pl-0") 64 | li 65 | div(class="float-left", style="height: 40px; width: 40px;") 66 | span 67 | i(class="fas fa-unlock-alt fa-2x mr-2", style="margin-top: 6px; margin-left: 3px;") 68 | 69 | span(class="font-weight-bold") Unconfirmed Transactions 70 | 71 | p(class="lead") #{mempoolInfo.size.toLocaleString()} tx 72 | - var mempoolBytesData = utils.formatLargeNumber(mempoolInfo.usage, 2); 73 | span(class="text-muted") (#{mempoolBytesData[0]} #{mempoolBytesData[1].abbreviation}B) 74 | 75 | li 76 | div(class="float-left", style="height: 40px; width: 40px; font-size: 12px;") 77 | span 78 | i(class="fas fa-dumbbell fa-2x mr-2", style="margin-top: 6px;") 79 | 80 | - var difficultyData = utils.formatLargeNumber(getblockchaininfo.difficulty, 3); 81 | 82 | span(class="font-weight-bold") Difficulty 83 | 84 | p(class="lead") 85 | span(title=parseFloat(getblockchaininfo.difficulty).toLocaleString(), data-toggle="tooltip") 86 | span #{difficultyData[0]} 87 | span x 10 88 | sup #{difficultyData[1].exponent} 89 | 90 | div(class="col-md-4") 91 | ul(style="list-style-type: none;", class="pl-0") 92 | li 93 | div(class="float-left", style="height: 40px; width: 40px; font-size: 12px;") 94 | span 95 | i(class="fas fa-money-bill-wave-alt fa-2x mr-2", style="margin-top: 7px;") 96 | 97 | span(class="font-weight-bold") Exchange Rate 98 | span(data-toggle="tooltip", title=("Exchange-rate data from: " + coinConfig.exchangeRateData.jsonUrl)) 99 | i(class="fas fa-info-circle") 100 | 101 | if (global.exchangeRate) 102 | p(class="lead") #{utils.formatExchangedCurrency(1.0)} 103 | else 104 | p(class="lead") - 105 | 106 | li 107 | div(class="float-left", style="height: 40px; width: 40px;") 108 | span 109 | i(class="fas fa-bolt fa-2x mr-2", style="margin-top: 6px; margin-left: 6px;") 110 | 111 | - var chainworkData = utils.formatLargeNumber(parseInt("0x" + getblockchaininfo.chainwork), 2); 112 | span(class="font-weight-bold") Chainwork 113 | 114 | p(class="lead") 115 | span(data-toggle="tooltip", title=getblockchaininfo.chainwork.replace(/^0+/, '')) 116 | span #{chainworkData[0]} 117 | span x 10 118 | sup #{chainworkData[1].exponent} 119 | span hashes 120 | 121 | include includes/tools-card.pug 122 | 123 | if (latestBlocks) 124 | div(class="card mb-3") 125 | div(class="card-header") 126 | div(class="row") 127 | div(class="col") 128 | h2(class="h6 mb-0") Latest Blocks 129 | if (getblockchaininfo.initialblockdownload) 130 | small (#{(getblockchaininfo.headers - getblockchaininfo.blocks).toLocaleString()} behind) 131 | 132 | div(class="col") 133 | span(style="float: right;") 134 | a(href="/blocks") 135 | i(class="fas fa-cubes") 136 | span Browse Blocks » 137 | 138 | div(class="card-body") 139 | 140 | - var blocks = latestBlocks; 141 | - var blockOffset = 0; 142 | 143 | include includes/blocks-list.pug 144 | 145 | 146 | if (chainTxStats) 147 | div(class="card mb-3") 148 | div(class="card-header") 149 | div(class="row") 150 | div(class="col") 151 | h2(class="h6 mb-0") Transaction Stats Summary 152 | 153 | div(class="col") 154 | span(style="float: right;") 155 | a(href="/tx-stats") 156 | i(class="fas fa-chart-bar") 157 | span Transaction Stats » 158 | 159 | div(class="card-body") 160 | table(class="table table-responsive-sm text-right mb-0") 161 | thead 162 | tr 163 | th 164 | each item, index in chainTxStatsLabels 165 | th #{item} 166 | tbody 167 | tr 168 | th(class="text-left") Count 169 | each item, index in chainTxStats 170 | td(class="monospace") #{item.window_tx_count.toLocaleString()} 171 | 172 | tr 173 | th(class="text-left") Rate 174 | each item, index in chainTxStats 175 | td(class="monospace") #{new Decimal(item.txrate).toDecimalPlaces(4)} 176 | 177 | block endOfBody 178 | script(async, defer, src="https://buttons.github.io/buttons.js") -------------------------------------------------------------------------------- /views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | meta(charset="utf-8") 5 | meta(name="viewport", content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, shrink-to-fit=no") 6 | 7 | if (session.uiTheme && session.uiTheme == "dark") 8 | link(rel="stylesheet", href="/css/bootstrap-dark.css") 9 | 10 | style. 11 | hr { background-color: #555555; } 12 | else 13 | link(rel="stylesheet", href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css", integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4", crossorigin="anonymous") 14 | 15 | link(rel="stylesheet", href="https://fonts.googleapis.com/css?family=Source+Code+Pro|Ubuntu:400,700") 16 | link(rel="stylesheet", href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css") 17 | link(rel="stylesheet", href="/css/radial-progress.css", type="text/css") 18 | link(rel='stylesheet', href='/css/styling.css') 19 | 20 | link(rel="icon", type="image/png", href=("/img/logo/" + config.coin.toLowerCase() + ".png")) 21 | script(src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js") 22 | script(src="https://dagrejs.github.io/project/dagre-d3/latest/dagre-d3.min.js") 23 | 24 | block headContent 25 | title Explorer 26 | 27 | body 28 | nav(class="navbar navbar-expand-lg navbar-dark bg-dark mb-4") 29 | div(class="container") 30 | a(class="navbar-brand", href="/") 31 | span 32 | if (coinConfig.logoUrl) 33 | img(src=coinConfig.logoUrl, class="header-image", alt="logo") 34 | span #{coinConfig.siteTitle} 35 | 36 | button(type="button", class="navbar-toggler navbar-toggler-right", data-toggle="collapse", data-target="#navbarNav", aria-label="collapse navigation") 37 | span(class="navbar-toggler-icon") 38 | 39 | div(class="collapse navbar-collapse", id="navbarNav") 40 | if (client) 41 | ul(class="navbar-nav mr-auto") 42 | li(class="nav-item") 43 | a(href="/about", class="nav-link") 44 | span About 45 | 46 | if (config.siteTools) 47 | li(class="nav-item dropdown") 48 | a(class="nav-link dropdown-toggle", href="javascript:void(0)", role="button", data-toggle="dropdown", aria-haspopup="true", aria-expanded="false") 49 | span Tools 50 | div(class="dropdown-menu", aria-label="Tools Items") 51 | each item in config.siteTools 52 | a(class="dropdown-item", href=item.url) 53 | i(class=item.fontawesome, style="width: 20px; margin-right: 10px;") 54 | span #{item.name} 55 | 56 | if (config.headerDropdownLinks) 57 | li(class="nav-item dropdown") 58 | a(class="nav-link dropdown-toggle", href="javascript:void(0)", role="button", data-toggle="dropdown", aria-haspopup="true", aria-expanded="false") 59 | span #{config.headerDropdownLinks.title} 60 | div(class="dropdown-menu", aria-label=(config.headerDropdownLinks.title + " Items")) 61 | each item in config.headerDropdownLinks.links 62 | a(class="dropdown-item", href=item.url) 63 | img(src=item.imgUrl, style="width: 24px; height: 24px; margin-right: 8px;", alt=item.name) 64 | span #{item.name} 65 | 66 | li(class="nav-item dropdown") 67 | a(class="nav-link dropdown-toggle", href="javascript:void(0)", id="navbarDropdown", role="button", data-toggle="dropdown", aria-haspopup="true", aria-expanded="false") 68 | i(class="fas fa-cog") 69 | span Settings 70 | div(class="dropdown-menu", aria-labelledby="navbarDropdown") 71 | if (coinConfig.currencyUnits) 72 | span(class="dropdown-header") Currency Units 73 | each item in coinConfig.currencyUnits 74 | a(class="dropdown-item", href=("/changeSetting?name=currencyFormatType&value=" + item.values[0])) 75 | each valueName in item.values 76 | if (currencyFormatType == valueName) 77 | i(class="fas fa-check") 78 | span #{item.name} 79 | 80 | span(class="dropdown-header") Theme 81 | a(class="dropdown-item", href="/changeSetting?name=uiTheme&value=light") 82 | if (session.uiTheme == "light" || session.uiTheme == "") 83 | i(class="fas fa-check") 84 | span Light 85 | a(class="dropdown-item", href="/changeSetting?name=uiTheme&value=dark") 86 | if (session.uiTheme == "dark") 87 | i(class="fas fa-check") 88 | span Dark 89 | 90 | form(method="post", action="/search", class="form-inline") 91 | div(class="input-group input-group-sm") 92 | input(type="text", class="form-control form-control-sm", name="query", placeholder="block height/hash, txid, address", value=(query), style="width: 300px;") 93 | div(class="input-group-append") 94 | button(type="submit", class="btn btn-primary") 95 | i(class="fas fa-search") 96 | 97 | if (host && port && !homepage) 98 | div(id="sub-menu", class="container mb-4", style="margin-top: -1.0rem;") 99 | ul(class="nav") 100 | each item, index in config.siteTools 101 | li(class="nav-item") 102 | a(href=item.url, class="nav-link") 103 | span #{item.name} 104 | 105 | 106 | hr 107 | 108 | div(class="container") 109 | if (userMessage) 110 | div(class="alert", class=(userMessageType ? ("alert-" + userMessageType) : "alert-warning"), role="alert") 111 | span !{userMessage} 112 | 113 | block content 114 | 115 | div(style="margin-bottom: 30px;") 116 | 117 | footer(class="footer bg-dark text-light pt-3 pb-1 px-3", style="margin-top: 50px;") 118 | div(class="container") 119 | div(class="row") 120 | div(class="col-md-5") 121 | dl 122 | dt Source 123 | dd 124 | a(href="https://github.com/waqas64/woc-explorer") github.com/waqas64/woc-explorer 125 | 126 | //- dt Running Version 127 | //- dd 128 | //- a(href=("https://github.com/waqas64/btc-rpc-explorer/commit/" + sourcecodeVersion)) #{sourcecodeVersion} 129 | //- span(style="color: #ccc;") (#{sourcecodeDate}) 130 | 131 | //- if (config.demoSite) 132 | //- dt Public Demos 133 | //- dd 134 | //- if (coinConfig.demoSiteUrl) 135 | //- a(href=coinConfig.demoSiteUrl) #{coinConfig.demoSiteUrl} 136 | //- else 137 | //- a(href="https://btc-explorer.chaintools.io") https://btc-explorer.chaintools.io 138 | 139 | //- div(class="mt-2") 140 | //- - var demoSiteCoins = ["BTC", "LTC"]; 141 | //- each demoSiteCoin in demoSiteCoins 142 | //- a(href=coinConfigs[demoSiteCoin].demoSiteUrl, class="mr-2", title=coinConfigs[demoSiteCoin].siteTitle) 143 | //- img(src=("/img/logo/" + demoSiteCoin.toLowerCase() + ".svg"), alt=demoSiteCoin.toLowerCase()) 144 | 145 | //- a(href="https://lightning.chaintools.io", class="mr-2", title="Lightning Explorer") 146 | //- img(src=("/img/logo/lightning.svg"), style="width: 32px; height: 32px;", alt="lightning") 147 | 148 | div(class="col-md-7 text-md-right") 149 | dl 150 | dt Support Development of #{coinConfig.siteTitle} 151 | dd 152 | div(class="text-md-right text-center") 153 | 154 | each coin, index in config.donationAddresses.coins 155 | div(style="display: inline-block; max-width: 150px;", class="text-center mb-3 word-wrap monospace", class=(index > 0 ? "ml-md-3" : false)) 156 | //img(src=donationAddressQrCodeUrls[coin], alt=config.donationAddresses[coin].address, style="border: solid 1px #ccc;") 157 | br 158 | if (coinConfig.ticker == coin) 159 | span #{coin} address: 160 | a(href=("/address/" + config.donationAddresses[coin].address)) #{"QR"} 161 | else 162 | span #{coin} address: 163 | a(href=(config.donationAddresses.sites[coin] + "/address/" + config.donationAddresses[coin].address)) #{"QR"} 164 | 165 | 166 | script(src="https://code.jquery.com/jquery-3.3.1.min.js", integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=", crossorigin="anonymous") 167 | script(src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js", integrity="sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ", crossorigin="anonymous") 168 | script(src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js", integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb", crossorigin="anonymous") 169 | script(src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js", integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm", crossorigin="anonymous") 170 | script(defer, src="https://use.fontawesome.com/releases/v5.2.0/js/all.js", integrity="sha384-4oV5EgaV02iISL2ban6c/RmotsABqE4yZxZLcYMAdG7FAPsyHYAPpywE9PJo+Khy", crossorigin="anonymous") 171 | 172 | script(src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js") 173 | 174 | script. 175 | $(document).ready(function() { 176 | $('[data-toggle="tooltip"]').tooltip(); 177 | $('[data-toggle="popover"]').popover({html:true, container:"body"}); 178 | }); 179 | 180 | hljs.initHighlightingOnLoad(); 181 | 182 | if (config.credentials.sentryUrl && config.credentials.sentryUrl.length > 0) 183 | script(src="https://browser.sentry-cdn.com/4.0.4/bundle.min.js", crossorigin="anonymous") 184 | script. 185 | Sentry.init({ dsn: '#{config.credentials.sentryUrl}' }); 186 | 187 | if (config.credentials.googleAnalyticsTrackingId && config.credentials.googleAnalyticsTrackingId.trim().length > 0) 188 | script(async, src=("https://www.googletagmanager.com/gtag/js?id=" + config.credentials.googleAnalyticsTrackingId)) 189 | script. 190 | window.dataLayer = window.dataLayer || []; 191 | function gtag(){dataLayer.push(arguments);} 192 | gtag('js', new Date()); 193 | 194 | gtag('config', '#{config.credentials.googleAnalyticsTrackingId}'); 195 | 196 | 197 | block endOfBody 198 | -------------------------------------------------------------------------------- /views/mempool-summary.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headContent 4 | title Mempool Summary 5 | 6 | block content 7 | h1(class="h3") Mempool Summary 8 | hr 9 | 10 | if (false) 11 | pre 12 | code #{JSON.stringify(mempoolstats, null, 4)} 13 | 14 | if (true) 15 | div(class="card mb-3") 16 | div(class="card-header") 17 | span(class="h6") Summary 18 | div(class="card-body") 19 | table(class="table details-table mb-0") 20 | tr 21 | td(class="properties-header") Transaction Count 22 | td #{getmempoolinfo.size.toLocaleString()} 23 | 24 | tr 25 | - var mem1Data = utils.formatLargeNumber(getmempoolinfo.usage, 2); 26 | - var mem2Data = utils.formatLargeNumber(getmempoolinfo.bytes, 2); 27 | 28 | td(class="properties-header") Memory Usage 29 | td(class="monospace") 30 | span #{mem1Data[0]} #{mem1Data[1].abbreviation}B 31 | span(class="text-muted") (virtual size: #{mem2Data[0]} #{mem2Data[1].abbreviation}B) 32 | 33 | tr 34 | td(class="properties-header") Total Fees 35 | td(class="monospace") 36 | - var currencyValue = mempoolstats["totalFees"]; 37 | include includes/value-display.pug 38 | 39 | if (getmempoolinfo.size > 0) 40 | tr 41 | td(class="properties-header") Average Fee 42 | td(class="monospace") #{utils.formatCurrencyAmount(mempoolstats["averageFee"], currencyFormatType)} 43 | if (global.exchangeRate) 44 | span 45 | span(data-toggle="tooltip", title=utils.formatExchangedCurrency(mempoolstats["averageFee"])) 46 | i(class="fas fa-exchange-alt") 47 | 48 | tr 49 | td(class="properties-header") Average Fee per Byte 50 | td(class="monospace") #{utils.formatCurrencyAmountInSmallestUnits(mempoolstats["averageFeePerByte"], 2)}/B 51 | 52 | if (getmempoolinfo.size > 0) 53 | h2(class="h5") Transactions by fee rate 54 | hr 55 | 56 | if (false) 57 | #{JSON.stringify(mempoolstats)} 58 | 59 | if (true) 60 | - var feeBucketLabels = [("[0 - " + mempoolstats["satoshiPerByteBucketMaxima"][0] + ")")]; 61 | each item, index in mempoolstats["satoshiPerByteBuckets"] 62 | if (index > 0 && index < mempoolstats["satoshiPerByteBuckets"].length - 1) 63 | - feeBucketLabels.push(("[" + mempoolstats["satoshiPerByteBucketMaxima"][index - 1] + " - " + mempoolstats["satoshiPerByteBucketMaxima"][index] + ")")); 64 | 65 | - feeBucketLabels.push((mempoolstats.satoshiPerByteBucketMaxima[mempoolstats.satoshiPerByteBucketMaxima.length - 1] + "+")); 66 | 67 | - var feeBucketTxCounts = mempoolstats["satoshiPerByteBucketCounts"]; 68 | - var totalfeeBuckets = mempoolstats["satoshiPerByteBucketTotalFees"]; 69 | 70 | canvas(id="mempoolBarChart", height="100", class="mb-4") 71 | 72 | script(src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.bundle.min.js") 73 | 74 | script var feeBucketLabels = []; 75 | script var bgColors = []; 76 | each feeBucketLabel, index in feeBucketLabels 77 | - var percentTx = Math.round(100 * feeBucketTxCounts[index] / getmempoolinfo.size).toLocaleString(); 78 | script feeBucketLabels.push(["#{feeBucketLabel}","#{feeBucketTxCounts[index]} tx (#{percentTx}%)"]); 79 | script bgColors.push("hsl(#{(333 * index / feeBucketLabels.length)}, 100%, 50%)"); 80 | 81 | script var feeBucketTxCounts = [#{feeBucketTxCounts}]; 82 | 83 | script. 84 | var ctx = document.getElementById("mempoolBarChart").getContext('2d'); 85 | var mempoolBarChart = new Chart(ctx, { 86 | type: 'bar', 87 | data: { 88 | labels: feeBucketLabels, 89 | datasets: [{ 90 | data: feeBucketTxCounts, 91 | backgroundColor: bgColors 92 | }] 93 | }, 94 | options: { 95 | legend: { 96 | display: false 97 | }, 98 | scales: { 99 | yAxes: [{ 100 | ticks: { 101 | beginAtZero:true 102 | } 103 | }] 104 | } 105 | } 106 | }); 107 | 108 | table(class="table table-striped table-responsive-sm mb-4") 109 | thead 110 | tr 111 | th Fee Rate 112 | th(class="text-right") Tx Count 113 | th(class="text-right") Total Fees 114 | th(class="text-right") Average Fee 115 | th(class="text-right") Average Fee Rate 116 | tbody 117 | each item, index in mempoolstats["satoshiPerByteBuckets"] 118 | tr 119 | td #{mempoolstats["satoshiPerByteBucketLabels"][index]} 120 | td(class="text-right monospace") #{item["count"].toLocaleString()} 121 | td(class="text-right monospace") #{utils.formatCurrencyAmount(item["totalFees"], currencyFormatType)} 122 | 123 | if (item["totalBytes"] > 0) 124 | - var avgFee = item["totalFees"] / item["count"]; 125 | - var avgFeeRate = item["totalFees"] / item["totalBytes"]; 126 | 127 | td(class="text-right monospace") #{utils.formatCurrencyAmount(avgFee, currencyFormatType)} 128 | if (global.exchangeRate) 129 | span 130 | span(data-toggle="tooltip", title=utils.formatExchangedCurrency(avgFee)) 131 | i(class="fas fa-exchange-alt") 132 | 133 | td(class="text-right monospace") #{utils.formatCurrencyAmountInSmallestUnits(avgFeeRate, 2)}/B 134 | else 135 | td(class="text-right monospace") - 136 | td(class="text-right monospace") - 137 | 138 | 139 | h2(class="h5") Transactions by size 140 | hr 141 | 142 | canvas(id="txSizesBarChart", height="100", class="mb-4") 143 | 144 | script var sizeBucketLabels = []; 145 | script var bgColors = []; 146 | each sizeBucketLabel, index in mempoolstats["sizeBucketLabels"] 147 | - var percentTx = Math.round(100 * mempoolstats["sizeBucketTxCounts"][index] / getmempoolinfo.size).toLocaleString(); 148 | script sizeBucketLabels.push(["#{sizeBucketLabel} bytes","#{mempoolstats["sizeBucketTxCounts"][index]} tx (#{percentTx}%)"]); 149 | script bgColors.push("hsl(#{(333 * index / mempoolstats["sizeBucketLabels"].length)}, 100%, 50%)"); 150 | 151 | script var sizeBucketTxCounts = [#{mempoolstats["sizeBucketTxCounts"]}]; 152 | 153 | script. 154 | var ctx = document.getElementById("txSizesBarChart").getContext('2d'); 155 | var txSizesBarChart = new Chart(ctx, { 156 | type: 'bar', 157 | data: { 158 | labels: sizeBucketLabels, 159 | datasets: [{ 160 | data: sizeBucketTxCounts, 161 | backgroundColor: bgColors 162 | }] 163 | }, 164 | options: { 165 | legend: { 166 | display: false 167 | }, 168 | scales: { 169 | yAxes: [{ 170 | ticks: { 171 | beginAtZero:true 172 | } 173 | }] 174 | } 175 | } 176 | }); 177 | 178 | h2(class="h5") Transactions by age 179 | hr 180 | 181 | canvas(id="txAgesBarChart", height="100", class="mb-4") 182 | 183 | script var ageBucketLabels = []; 184 | script var bgColors = []; 185 | each ageBucketLabel, index in mempoolstats["ageBucketLabels"] 186 | - var percentTx = Math.round(100 * mempoolstats["ageBucketTxCounts"][index] / getmempoolinfo.size).toLocaleString(); 187 | script ageBucketLabels.push(["#{ageBucketLabel} sec","#{mempoolstats["ageBucketTxCounts"][index]} tx (#{percentTx}%)"]); 188 | script bgColors.push("hsl(#{(333 * index / mempoolstats["ageBucketLabels"].length)}, 100%, 50%)"); 189 | 190 | script var ageBucketTxCounts = [#{mempoolstats["ageBucketTxCounts"]}]; 191 | 192 | script. 193 | var ctx = document.getElementById("txAgesBarChart").getContext('2d'); 194 | var txAgesBarChart = new Chart(ctx, { 195 | type: 'bar', 196 | data: { 197 | labels: ageBucketLabels, 198 | datasets: [{ 199 | data: ageBucketTxCounts, 200 | backgroundColor: bgColors 201 | }] 202 | }, 203 | options: { 204 | legend: { 205 | display: false 206 | }, 207 | scales: { 208 | yAxes: [{ 209 | ticks: { 210 | beginAtZero:true 211 | } 212 | }] 213 | } 214 | } 215 | }); 216 | -------------------------------------------------------------------------------- /views/node-status.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headContent 4 | title Node Status 5 | 6 | block content 7 | h1(class="h3") Node Status 8 | hr 9 | 10 | if (getblockchaininfo) 11 | if (false) 12 | p Data from RPC commands 13 | a(href="/rpc-browser?method=getblockchaininfo") getblockchaininfo 14 | span , 15 | a(href="/rpc-browser?method=getnetworkinfo") getnetworkinfo 16 | span , and 17 | a(href="/rpc-browser?method=getnettotals") getnettotals 18 | 19 | if (false) 20 | pre 21 | code #{JSON.stringify(getblockchaininfo, null, 4)} 22 | 23 | if (global.client) 24 | div(class="card mb-3") 25 | div(class="card-header") 26 | span(class="h6") Summary 27 | div(class="card-body") 28 | table(class="table details-table mb-0") 29 | tr 30 | td(class="properties-header") Host : Port 31 | td(class="monospace") #{global.client.host + " : " + global.client.port} 32 | 33 | tr 34 | td(class="properties-header") Chain 35 | td(class="monospace") #{getblockchaininfo.chain} 36 | tr 37 | td(class="properties-header") Version 38 | td(class="monospace") #{getnetworkinfo.version} 39 | span(class="monospace") (#{getnetworkinfo.subversion}) 40 | tr 41 | td(class="properties-header") Protocol Version 42 | td(class="monospace") #{getnetworkinfo.protocolversion} 43 | 44 | if (getblockchaininfo.size_on_disk) 45 | - var sizeData = utils.formatLargeNumber(getblockchaininfo.size_on_disk, 2); 46 | tr 47 | td(class="properties-header") Blockchain Size 48 | td(class="monospace") #{sizeData[0]} #{sizeData[1].abbreviation}B 49 | br 50 | span(class="text-muted") (pruned: #{getblockchaininfo.pruned}) 51 | tr 52 | td(class="properties-header") Connections 53 | td(class="monospace") #{getnetworkinfo.connections.toLocaleString()} 54 | 55 | tr 56 | td(class="properties-header") Block Count 57 | td(class="monospace") #{getblockchaininfo.blocks.toLocaleString()} 58 | br 59 | span(class="text-muted") (headers: #{getblockchaininfo.headers.toLocaleString()}) 60 | tr 61 | - var scales = [ {val:1000000000000000, name:"quadrillion"}, {val:1000000000000, name:"trillion"}, {val:1000000000, name:"billion"}, {val:1000000, name:"million"} ]; 62 | - var scaleDone = false; 63 | td(class="properties-header") Difficulty 64 | td(class="monospace") 65 | - var difficultyData = utils.formatLargeNumber(getblockchaininfo.difficulty, 3); 66 | 67 | span(title=parseFloat(getblockchaininfo.difficulty).toLocaleString(), data-toggle="tooltip") 68 | span #{difficultyData[0]} 69 | span x 10 70 | sup #{difficultyData[1].exponent} 71 | 72 | tr 73 | td(class="properties-header") Status 74 | td(class="monospace") 75 | if (getblockchaininfo.initialblockdownload || getblockchaininfo.headers > getblockchaininfo.blocks) 76 | span Initial block download progress #{(100 * getblockchaininfo.verificationprogress).toLocaleString()}% 77 | else 78 | span Synchronized with network 79 | 80 | - var startTimeAgo = moment.duration(uptimeSeconds * 1000); 81 | tr 82 | td(class="properties-header") Uptime 83 | td(class="monospace") #{startTimeAgo.format()} 84 | 85 | tr 86 | td(class="properties-header") Network Traffic 87 | td(class="monospace") 88 | - var downData = utils.formatLargeNumber(getnettotals.totalbytesrecv, 2); 89 | - var downRateData = utils.formatLargeNumber(getnettotals.totalbytesrecv / uptimeSeconds, 2); 90 | - var upData = utils.formatLargeNumber(getnettotals.totalbytessent, 2); 91 | - var upRateData = utils.formatLargeNumber(getnettotals.totalbytessent / uptimeSeconds, 2); 92 | 93 | span Total Download: #{downData[0]} #{downData[1].abbreviation}B 94 | span(class="text-muted") (avg #{downRateData[0]} #{downRateData[1].abbreviation}B/s) 95 | br 96 | span Total Upload: #{upData[0]} #{upData[1].abbreviation}B 97 | span(class="text-muted") (avg #{upRateData[0]} #{upRateData[1].abbreviation}B/s) 98 | 99 | tr 100 | td(class="properties-header") Warnings 101 | td(class="monospace") 102 | if (getblockchaininfo.warnings && getblockchaininfo.warnings.trim().length > 0) 103 | span #{getblockchaininfo.warnings} 104 | else 105 | span None 106 | 107 | else 108 | div(class="alert alert-warning") No active RPC connection 109 | -------------------------------------------------------------------------------- /views/notifications.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headContent 4 | title Node Status 5 | 6 | block content 7 | h1(class="h3") Notifications 8 | hr 9 | 10 | 11 | div(class="card mb-3") 12 | div(class="card-header") 13 | span(class="h4") New Block Mined 14 | div(class="card-body") 15 | table(class="table details-table mb-0") 16 | tr 17 | td(class="monospace") 18 | span(class="h5" style="height: 50px; width: 40px; margin-right: 10px;") 19 | 20 | a(href="https://t.me/BlockNotifications" target="_blank") Telegram 21 | tr 22 | td(class="monospace") 23 | span(class="h5" style="height: 50px; width: 40px; margin-right: 10px;") 24 | 25 | a(href="https://join.slack.com/t/blocknotifications/shared_invite/enQtNDkzMzE0MDUzMDYxLWMzNDdhYjVmYmVkZmZjM2EzYjg2NjE1ZjY3ZTAxYjJkOWJkNGUyNGQ2YWY2MmM2MWFkOGNhOTI4MmFmODhkZWI …" target="_blank") Slack 26 | tr 27 | td(class="monospace") 28 | span(class="h5" style="height: 50px; width: 40px; margin-right: 10px;") 29 | 30 | a(href="https://twitter.com/BlockNotificat1" target="_blank") Twitter 31 | -------------------------------------------------------------------------------- /views/peers.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headContent 4 | title Peers 5 | 6 | link(rel="stylesheet", href="https://unpkg.com/leaflet@1.3.3/dist/leaflet.css", integrity="sha512-Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ==", crossorigin="") 7 | 8 | script(src="https://unpkg.com/leaflet@1.3.3/dist/leaflet.js", integrity="sha512-tAGcCfR4Sc5ZP5ZoVz0quoZDYX5aCtEm/eu1KhSLj2c9eFrylXZknQYmxUssFaVJKvvc0dJQixhGjG2yXWiV9Q==", crossorigin="") 9 | 10 | style. 11 | .versions-hidden-rows, .services-hidden-rows { 12 | display: none; 13 | } 14 | 15 | #map { height: 700px; } 16 | 17 | block content 18 | h1(class="h3") Peers 19 | hr 20 | 21 | ul(class='nav nav-tabs mb-3') 22 | li(class="nav-item") 23 | a(data-toggle="tab", href="#tab-summary", class="nav-link active", role="tab") Summary 24 | li(class="nav-item") 25 | a(data-toggle="tab", href="#tab-details", class="nav-link", role="tab") Details 26 | li(class="nav-item") 27 | a(data-toggle="tab", href="#tab-json", class="nav-link", role="tab") JSON 28 | 29 | div(class="tab-content") 30 | div(id="tab-summary", class="tab-pane active", role="tabpanel") 31 | h2(class="h5 mb-3") Connected to #{peerSummary.getpeerinfo.length} 32 | if (peerSummary.getpeerinfo.length == 1) 33 | span Peer 34 | else 35 | span Peers 36 | 37 | if (config.credentials.ipStackComApiAccessKey && config.credentials.ipStackComApiAccessKey.trim().length > 0) 38 | div(id="map", class="mb-4") 39 | 40 | div(class="card mb-4") 41 | div(class="card-header") 42 | h2(class="h6 mb-0") Top Versions 43 | div(class="card-body") 44 | table(class="table table-striped table-responsive-sm") 45 | thead 46 | tr 47 | th 48 | th(class="data-header") Version 49 | th(class="data-header") Count 50 | tbody 51 | each item, index in peerSummary.versionSummary 52 | tr(class=(index >= 5 ? "versions-hidden-rows" : false)) 53 | th(class="data-cell") #{index + 1} 54 | 55 | td(class="data-cell") #{item[0]} 56 | td(class="data-cell") #{item[1].toLocaleString()} 57 | 58 | div(class="card mb-4") 59 | div(class="card-header") 60 | h2(class="h6 mb-0") Top Service Flags 61 | div(class="card-body") 62 | table(class="table table-striped table-responsive-sm") 63 | thead 64 | tr 65 | th 66 | th(class="data-header") Services 67 | th(class="data-header") Count 68 | tbody 69 | each item, index in peerSummary.servicesSummary 70 | tr(class=(index >= 5 ? "services-hidden-rows" : false)) 71 | th(class="data-cell") #{index + 1} 72 | 73 | td(class="data-cell") #{item[0]} 74 | td(class="data-cell") #{item[1].toLocaleString()} 75 | 76 | 77 | 78 | div(id="tab-details", class="tab-pane", role="tabpanel") 79 | table(class="table table-striped table-responsive-sm mt-4") 80 | thead 81 | tr 82 | th 83 | th(class="data-header") Version 84 | th(class="data-header") Address 85 | th(class="data-header") Services 86 | th(class="data-header") Location 87 | th(class="data-header") Last Send / Receive 88 | 89 | tbody 90 | each item, index in peerSummary.getpeerinfo 91 | - var lastSendAgo = moment.duration(moment.utc(new Date()).diff(moment.utc(new Date(parseInt(item.lastsend) * 1000)))).format().replace("milliseconds", "ms"); 92 | - var lastRecvAgo = moment.duration(moment.utc(new Date()).diff(moment.utc(new Date(parseInt(item.lastrecv) * 1000)))).format().replace("milliseconds", "ms"); 93 | 94 | tr 95 | th(class="data-cell") #{index + 1} 96 | 97 | td(class="data-cell") #{item.subver} 98 | td(class="data-cell") #{item.addr} 99 | td(class="data-cell") #{item.services} 100 | td(class="data-cell") 101 | - var ipAddr = item.addr.substring(0, item.addr.lastIndexOf(":")); 102 | if (peerIpSummary.ips.includes(ipAddr)) 103 | - var ipDetails = peerIpSummary.detailsByIp[ipAddr]; 104 | if (ipDetails.city) 105 | span #{ipDetails.city}, 106 | if (ipDetails.country_name) 107 | span #{ipDetails.country_name} 108 | if (ipDetails.location && ipDetails.location.country_flag_emoji) 109 | span #{ipDetails.location.country_flag_emoji} 110 | else 111 | span ? 112 | 113 | - var ipAddr = null; 114 | 115 | td(class="data-cell") #{lastSendAgo} / #{lastRecvAgo} 116 | 117 | 118 | div(id="tab-json", class="tab-pane", role="tabpanel") 119 | each item, index in peerSummary.getpeerinfo 120 | div(class="border-bottom p-1") 121 | a(href="javascript:void(0)" onclick=("javascript:var peer = document.getElementById('peerinfo_" + index + "'); peer.style.display = peer.style.display === 'none' ? '' : 'none';")) 122 | i(class="fas fa-plus-circle") 123 | 124 | span(class="monospace") #{item.addr} 125 | 126 | div(style="display: none;", id=("peerinfo_" + index), class="p-3") 127 | h6 Peer Details 128 | pre 129 | code #{JSON.stringify(item, null, 4)} 130 | 131 | if (peerIpSummary.detailsByIp[item.addr.substring(0, item.addr.lastIndexOf(":"))]) 132 | hr 133 | 134 | h6 IP Geo-Location Info 135 | pre 136 | code #{JSON.stringify(peerIpSummary.detailsByIp[item.addr.substring(0, item.addr.lastIndexOf(":"))], null, 4)} 137 | 138 | 139 | block endOfBody 140 | if (config.credentials.ipStackComApiAccessKey && config.credentials.ipStackComApiAccessKey.trim().length > 0) 141 | script. 142 | var mymap = L.map('map').setView([21.505, -0.09], 3); 143 | 144 | L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', { 145 | maxZoom: 18, 146 | attribution: 'Map data © OpenStreetMap contributors, ' + 147 | 'CC-BY-SA, ' + 148 | 'Imagery © Mapbox', 149 | id: 'mapbox.streets' 150 | }).addTo(mymap); 151 | 152 | /*L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { 153 | attribution: '© OpenStreetMap contributors' 154 | }).addTo(mymap);*/ 155 | 156 | $(document).ready(function() { 157 | window.dispatchEvent(new Event('resize')); 158 | }); 159 | 160 | each ipAddress, index in peerIpSummary.ips 161 | - var ipDetails = peerIpSummary.detailsByIp[ipAddress]; 162 | if (ipDetails && ipDetails.latitude && ipDetails.longitude) 163 | - var ipDetailsPopupHtml = "" + ipAddress + "
"; 164 | if (ipDetails.city) 165 | - var ipDetailsPopupHtml = ipDetailsPopupHtml + ipDetails.city + ", "; 166 | 167 | if (ipDetails.country_name) 168 | - var ipDetailsPopupHtml = ipDetailsPopupHtml + ipDetails.country_name + " "; 169 | 170 | if (ipDetails.location && ipDetails.location.country_flag_emoji) 171 | - var ipDetailsPopupHtml = ipDetailsPopupHtml + ipDetails.location.country_flag_emoji; 172 | 173 | script L.marker([#{ipDetails.latitude}, #{ipDetails.longitude}]).addTo(mymap).bindPopup("!{ipDetailsPopupHtml}"); 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /views/search.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headContent 4 | title Search 5 | 6 | block content 7 | h1(class="h3") Search 8 | hr 9 | 10 | div(class="mb-5") 11 | form(method="post", action="/search", class="form") 12 | div(class="input-group input-group-lg") 13 | input(type="text", class="form-control form-control-sm", name="query", placeholder="block height/hash, txid, address", value=(query), style="width: 300px;") 14 | div(class="input-group-append") 15 | button(type="submit", class="btn btn-primary") Search -------------------------------------------------------------------------------- /views/terminal.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headContent 4 | title RPC Terminal 5 | 6 | block content 7 | div(class="row") 8 | div(class="col") 9 | h1(class="h3") RPC Terminal 10 | 11 | div(class="col") 12 | if (!config.demoSite && (!config.credentials.rpc || !config.credentials.rpc.rpc)) 13 | span(style="float: right;") 14 | a(href="/disconnect", class="btn btn-secondary") Disconnect from node 15 | 16 | hr 17 | 18 | :markdown-it 19 | Use this interactive terminal to send RPC commands to your node. Results will be shown inline. To browse all available RPC commands you can use the [RPC Browser](/rpc-browser). 20 | 21 | div(class="card mb-3") 22 | div(class="card-body") 23 | form(id="terminal-form") 24 | div(class="form-group") 25 | label(for="input-cmd") Command 26 | input(type="text", id="input-cmd", name="cmd", class="form-control") 27 | 28 | input(type="submit", class="btn btn-primary btn-block", value="Send") 29 | 30 | hr 31 | 32 | div(id="terminal-output") 33 | 34 | block endOfBody 35 | script. 36 | $(document).ready(function() { 37 | $("#terminal-form").submit(function(e) { 38 | e.preventDefault(); 39 | 40 | var cmd = $("#input-cmd").val() 41 | 42 | var postData = {}; 43 | postData.cmd = cmd; 44 | 45 | $.post( 46 | "/rpc-terminal", 47 | postData, 48 | function(response, textStatus, jqXHR) { 49 | var t = new Date().getTime(); 50 | 51 | $("#terminal-output").prepend("
" + cmd + "
" + response + "
"); 52 | console.log(response); 53 | 54 | $("#output-" + t + " pre code").each(function(i, block) { 55 | hljs.highlightBlock(block); 56 | }); 57 | 58 | return false; 59 | }) 60 | .done(function(data) { 61 | }); 62 | 63 | return false; 64 | }); 65 | }); -------------------------------------------------------------------------------- /views/transaction.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headContent 4 | title Transaction #{txid} 5 | style. 6 | .field { 7 | word-wrap: break-word; 8 | } 9 | 10 | 11 | block content 12 | if (result && result.getrawtransaction) 13 | h1(class="h3 word-wrap") Transaction 14 | br 15 | small(class="monospace") #{txid} 16 | hr 17 | 18 | ul(class='nav nav-tabs mb-3') 19 | li(class="nav-item") 20 | a(data-toggle="tab", href="#tab-details", class="nav-link active", role="tab") Details 21 | li(class="nav-item") 22 | a(data-toggle="tab", href="#tab-scripts", class="nav-link", role="tab") Scripts 23 | li(class="nav-item") 24 | a(data-toggle="tab", href="#tab-json", class="nav-link", role="tab") JSON 25 | 26 | - DecimalRounded = Decimal.clone({ precision: 4, rounding: 2 }) 27 | 28 | - var totalInputValue = new Decimal(0); 29 | if (result.getrawtransaction.vin[0].coinbase) 30 | - totalInputValue = totalInputValue.plus(new Decimal(coinConfig.blockRewardFunction(result.getblock.height))); 31 | each txInput, txInputIndex in result.txInputs 32 | if (txInput && txInput.vout && result && result.getrawtransaction && result.getrawtransaction.vin ) 33 | - var vout = txInput.vout[result.getrawtransaction.vin[txInputIndex].vout]; 34 | if (vout.value) 35 | - totalInputValue = totalInputValue.plus(new Decimal(vout.value)); 36 | 37 | - var totalOutputValue = new Decimal(0); 38 | each vout, voutIndex in result.getrawtransaction.vout 39 | - totalOutputValue = totalOutputValue.plus(new Decimal(vout.value)); 40 | 41 | div(class="tab-content") 42 | div(id="tab-details", class="tab-pane active", role="tabpanel") 43 | if (global.specialTransactions && global.specialTransactions[txid]) 44 | div(class="alert alert-primary", style="padding-bottom: 0;") 45 | div(class="float-left", style="width: 55px; height: 55px; font-size: 18px;") 46 | i(class="fas fa-certificate fa-2x", style="margin-top: 10px;") 47 | 48 | h4(class="alert-heading h6 font-weight-bold") #{coinConfig.name} Fun 49 | 50 | // special transaction info 51 | - var stInfo = global.specialTransactions[txid]; 52 | if (stInfo.alertBodyHtml) 53 | p 54 | span !{stInfo.alertBodyHtml} 55 | 56 | if (stInfo.referenceUrl && stInfo.referenceUrl.trim().length > 0 && stInfo.alertBodyHtml.indexOf(stInfo.referenceUrl) == -1) 57 | span 58 | a(href=stInfo.referenceUrl) Read more 59 | else 60 | p 61 | span #{stInfo.summary} 62 | 63 | if (stInfo.referenceUrl && stInfo.referenceUrl.trim().length > 0) 64 | span 65 | a(href=stInfo.referenceUrl) Read more 66 | 67 | - var isTxConfirmed = true; 68 | if (!result.getrawtransaction.confirmations || result.getrawtransaction.confirmations == 0) 69 | - isTxConfirmed = false; 70 | 71 | div(class="card mb-3") 72 | div(class="card-header") 73 | span(class="h6") Summary 74 | div(class="card-body") 75 | if (!isTxConfirmed) 76 | div(class="row") 77 | div(class="summary-table-label") Status 78 | div(class="summary-table-content monospace") 79 | span(class="text-danger") Unconfirmed 80 | 81 | if (isTxConfirmed) 82 | div(class="row") 83 | div(class="summary-table-label") Block 84 | div(class="summary-table-content monospace") 85 | a(href=("/block/" + result.getrawtransaction.blockhash)) #{result.getrawtransaction.blockhash} 86 | if (result.getblock.height) 87 | br 88 | span (##{result.getblock.height.toLocaleString()}) 89 | 90 | if (isTxConfirmed) 91 | div(class="row") 92 | div(class="summary-table-label") Timestamp 93 | div(class="summary-table-content monospace") 94 | if (result.getrawtransaction.time) 95 | td(class="monospace") #{moment.utc(new Date(result.getrawtransaction["time"] * 1000)).format("Y-MM-DD HH:mm:ss")} utc 96 | - var timeAgoTime = result.getrawtransaction["time"]; 97 | include includes/time-ago.pug 98 | 99 | div(class="row") 100 | div(class="summary-table-label") Version 101 | div(class="summary-table-content monospace") #{result.getrawtransaction.version} 102 | 103 | if (result.getrawtransaction.vsize && result.getrawtransaction.vsize != result.getrawtransaction.size) 104 | div(class="row") 105 | div(class="summary-table-label") Virtual Size 106 | div(class="summary-table-content monospace") #{result.getrawtransaction.vsize.toLocaleString()} VB 107 | 108 | div(class="row") 109 | div(class="summary-table-label") Size 110 | div(class="summary-table-content monospace") #{result.getrawtransaction.size.toLocaleString()} B 111 | 112 | if (result.getrawtransaction.locktime > 0) 113 | div(class="row") 114 | div(class="summary-table-label") Locktime 115 | div(class="summary-table-content monospace") 116 | if (result.getrawtransaction.locktime < 500000000) 117 | span Spendable in block 118 | a(href=("/block-height/" + result.getrawtransaction.locktime)) #{result.getrawtransaction.locktime.toLocaleString()} 119 | span or later 120 | a(href="https://bitcoin.org/en/developer-guide#locktime-and-sequence-number", data-toggle="tooltip", title="More info about locktime", target="_blank") 121 | i(class="fas fa-info-circle") 122 | else 123 | span Spendable after #{moment.utc(new Date(result.getrawtransaction.locktime * 1000)).format("Y-MM-DD HH:mm:ss")} (utc) 124 | a(href="https://bitcoin.org/en/developer-guide#locktime-and-sequence-number", data-toggle="tooltip", title="More info about locktime", target="_blank") 125 | i(class="fas fa-info-circle") 126 | 127 | if (isTxConfirmed) 128 | div(class="row") 129 | div(class="summary-table-label") Confirmations 130 | div(class="summary-table-content monospace") 131 | if (!result.getrawtransaction.confirmations || result.getrawtransaction.confirmations == 0) 132 | strong(class="text-danger") 0 (unconfirmed) 133 | 134 | else if (result.getrawtransaction.confirmations < 6) 135 | strong(class="text-warning") #{result.getrawtransaction.confirmations} 136 | 137 | else 138 | strong(class="text-success") #{result.getrawtransaction.confirmations.toLocaleString()} 139 | 140 | 141 | if (result.getrawtransaction.vin[0].coinbase) 142 | div(class="row") 143 | div(class="summary-table-label") Fees Collected 144 | div(class="summary-table-content monospace") 145 | - var currencyValue = new Decimal(totalOutputValue).minus(totalInputValue); 146 | include includes/value-display.pug 147 | 148 | - var blockRewardMax = coinConfig.blockRewardFunction(result.getblock.height); 149 | if (totalOutputValue < blockRewardMax) 150 | div(class="row") 151 | div(class="summary-table-label") Fees Destroyed 152 | div(class="summary-table-content monospace") 153 | - var currencyValue = new Decimal(blockRewardMax).minus(totalOutputValue); 154 | include includes/value-display.pug 155 | 156 | a(class="ml-2", data-toggle="tooltip", title="The miner of this transaction's block failed to collect this value. As a result, it is lost.") 157 | i(class="fas fa-info-circle") 158 | 159 | - var minerInfo = utils.getMinerFromCoinbaseTx(result.getrawtransaction); 160 | div(class="row") 161 | div(class="summary-table-label") Miner 162 | div(class="summary-table-content monospace") 163 | if (minerInfo) 164 | span #{minerInfo.name} 165 | if (minerInfo.identifiedBy) 166 | span(data-toggle="tooltip", title=("Identified by: " + minerInfo.identifiedBy)) 167 | i(class="fas fa-info-circle") 168 | else 169 | span ? 170 | span(data-toggle="tooltip", title="Unable to identify miner") 171 | i(class="fas fa-info-circle") 172 | 173 | else 174 | 175 | div(class="row") 176 | div(class="summary-table-label") Fee Paid 177 | div(class="summary-table-content monospace") 178 | - var currencyValue = new Decimal(totalInputValue).minus(totalOutputValue); 179 | include includes/value-display.pug 180 | 181 | br 182 | span(class="text-muted") (#{utils.formatCurrencyAmount(totalInputValue, currencyFormatType)} - #{utils.formatCurrencyAmount(totalOutputValue, currencyFormatType)}) 183 | 184 | div(class="row") 185 | div(class="summary-table-label") Fee Rate 186 | div(class="summary-table-content monospace") 187 | if (result.getrawtransaction.vsize && result.getrawtransaction.vsize != result.getrawtransaction.size) 188 | span #{utils.addThousandsSeparators(new DecimalRounded(totalInputValue).minus(totalOutputValue).dividedBy(result.getrawtransaction.vsize).times(100000000))} sat/VB 189 | br 190 | 191 | span #{utils.addThousandsSeparators(new DecimalRounded(totalInputValue).minus(totalOutputValue).dividedBy(result.getrawtransaction.size).times(100000000))} sat/B 192 | 193 | 194 | if (result.getrawtransaction.vin[0].coinbase) 195 | div(class="card mb-3") 196 | div(class="card-header") 197 | h2(class="h6 mb-0") Coinbase 198 | div(class="card-body") 199 | h6 Hex 200 | div(style="background-color: #f0f0f0; padding: 5px 10px;", class="mb-3") 201 | span(class="monospace word-wrap") #{result.getrawtransaction.vin[0].coinbase} 202 | 203 | h6 Decoded 204 | div(style="background-color: #f0f0f0; padding: 5px 10px;", class="mb-3") 205 | span(class="monospace word-wrap") #{utils.hex2ascii(result.getrawtransaction.vin[0].coinbase)} 206 | 207 | div(class="card mb-3") 208 | div(class="card-header") 209 | h2(class="h6 mb-0") 210 | span #{result.getrawtransaction.vin.length.toLocaleString()} 211 | if (result.getrawtransaction.vin.length == 1) 212 | span Input 213 | else 214 | span Inputs 215 | 216 | span , 217 | 218 | span #{result.getrawtransaction.vout.length.toLocaleString()} 219 | if (result.getrawtransaction.vout.length == 1) 220 | span Output 221 | else 222 | span Outputs 223 | 224 | 225 | div(class="card-body") 226 | - var tx = result.getrawtransaction; 227 | - var txInputs = result.txInputs; 228 | - var blockHeight = -1; 229 | if (result && result.getblock) 230 | - blockHeight = result.getblock.height; 231 | include includes/transaction-io-details.pug 232 | 233 | - var fontawesomeInputName = "sign-in-alt"; 234 | - var fontawesomeOutputName = "sign-out-alt"; 235 | 236 | div(id="tab-scripts", class="tab-pane", role="tabpanel") 237 | div(class="card mb-3") 238 | div(class="card-header") 239 | span(class="h6") Input Scripts 240 | div(class="card-body") 241 | table(class="table table-striped mb-5") 242 | thead 243 | tr 244 | th(style="width: 50px;") 245 | th Script Sig (asm) 246 | tbody 247 | each vin, vinIndex in result.getrawtransaction.vin 248 | tr 249 | th(class="pl-0") 250 | a(data-toggle="tooltip", title=("Input #" + (vinIndex + 1)), style="white-space: nowrap;") 251 | i(class=("fas fa-" + fontawesomeInputName + " mr-2")) 252 | span #{(vinIndex + 1)} 253 | 254 | td 255 | if (vin.scriptSig && vin.scriptSig.asm) 256 | span(class="word-wrap monospace") #{vin.scriptSig.asm} 257 | 258 | else if (vin.coinbase) 259 | div(style="line-height: 1.75em;") 260 | span(class="tag") coinbase 261 | br 262 | span(class="word-wrap monospace") #{vin.coinbase} 263 | br 264 | span(class="word-wrap monospace text-muted") (decoded) #{utils.hex2ascii(vin.coinbase)} 265 | 266 | div(class="card mb-3") 267 | div(class="card-header") 268 | span(class="h6") Output Scripts 269 | div(class="card-body") 270 | table(class="table table-striped") 271 | thead 272 | tr 273 | th(style="width: 50px;") 274 | th Script Pub Key (asm) 275 | tbody 276 | each vout, voutIndex in result.getrawtransaction.vout 277 | tr 278 | th(class="pl-0") 279 | a(data-toggle="tooltip", title=("Output #" + (voutIndex + 1)), style="white-space: nowrap;") 280 | i(class=("fas fa-" + fontawesomeOutputName + " mr-2")) 281 | span #{(voutIndex + 1)} 282 | 283 | td 284 | if (vout.scriptPubKey && vout.scriptPubKey.asm) 285 | span(class="word-wrap monospace") #{vout.scriptPubKey.asm} 286 | if (vout.scriptPubKey.asm.startsWith("OP_RETURN")) 287 | br 288 | span(class="word-wrap monospace text-muted") (decoded) #{new Buffer(vout.scriptPubKey.hex, 'hex').toString('utf8')} 289 | 290 | div(id="tab-json", class="tab-pane", role="tabpanel") 291 | div(class="highlight") 292 | pre 293 | code(class="language-json", data-lang="json") #{JSON.stringify(result.getrawtransaction, null, 4)} 294 | 295 | //pre #{JSON.stringify(result.txInputs, null, 4)} 296 | 297 | 298 | -------------------------------------------------------------------------------- /views/tx-stats.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headContent 4 | title Transaction Stats 5 | 6 | block content 7 | h1(class="h3") Transaction Stats 8 | hr 9 | 10 | if (false) 11 | pre 12 | code #{JSON.stringify(txStatResults, null, 4)} 13 | 14 | if (true) 15 | if (false) 16 | #{JSON.stringify(txStats.txCounts.length)} 17 | 18 | if (true) 19 | canvas(id="graph1", class="mb-4") 20 | 21 | canvas(id="graph2", class="mb-4") 22 | 23 | script(src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.bundle.min.js") 24 | 25 | script var txCountData = []; 26 | each item, index in txStats.txCounts 27 | script txCountData.push({x:#{item.x}, y:#{item.y}}); 28 | 29 | script var txRateData = []; 30 | each item, index in txStats.txRates 31 | script txRateData.push({x:#{item.x}, y:#{item.y}}); 32 | 33 | script. 34 | var ctx1 = document.getElementById("graph1").getContext('2d'); 35 | var graph1 = new Chart(ctx1, { 36 | type: 'line', 37 | labels: [#{txStats.txLabels}], 38 | data: { 39 | datasets: [{ 40 | borderColor: '#36a2eb', 41 | backgroundColor: '#84CBFA', 42 | data: txCountData 43 | }] 44 | }, 45 | options: { 46 | title: { 47 | display: true, 48 | text: 'Cumulative Transactions' 49 | }, 50 | legend: { 51 | display: false 52 | }, 53 | scales: { 54 | xAxes: [{ 55 | type: 'linear', 56 | position: 'bottom', 57 | scaleLabel: { 58 | display: true, 59 | labelString: 'Block' 60 | }, 61 | ticks: { 62 | min: 0, 63 | stepSize: 25000, 64 | callback: function(value, index, values) { 65 | if (value > 1000000) { 66 | return (value / 1000000).toLocaleString() + "M"; 67 | 68 | } else if (value > 1000) { 69 | return (value / 1000).toLocaleString() + "k"; 70 | 71 | } else { 72 | return value; 73 | } 74 | } 75 | } 76 | }], 77 | yAxes: [{ 78 | scaleLabel: { 79 | display: true, 80 | labelString: 'Tx Count' 81 | }, 82 | ticks: { 83 | beginAtZero:true, 84 | min: 0, 85 | callback: function(value, index, values) { 86 | return (value / 1000000).toLocaleString() + "M"; 87 | } 88 | } 89 | }] 90 | } 91 | } 92 | }); 93 | 94 | 95 | 96 | var ctx2 = document.getElementById("graph2").getContext('2d'); 97 | var graph2 = new Chart(ctx2, { 98 | type: 'line', 99 | labels: [#{txStats.txLabels}], 100 | data: { 101 | datasets: [{ 102 | borderColor: '#36a2eb', 103 | backgroundColor: '#84CBFA', 104 | data: txRateData 105 | }] 106 | }, 107 | options: { 108 | title: { 109 | display: true, 110 | text: 'Average Transactions Per Second' 111 | }, 112 | legend: { 113 | display: false 114 | }, 115 | scales: { 116 | xAxes: [{ 117 | type: 'linear', 118 | position: 'bottom', 119 | scaleLabel: { 120 | display: true, 121 | labelString: 'Block' 122 | }, 123 | ticks: { 124 | min: 0, 125 | stepSize: 25000, 126 | callback: function(value, index, values) { 127 | if (value > 1000000) { 128 | return (value / 1000000).toLocaleString() + "M"; 129 | 130 | } else if (value > 1000) { 131 | return (value / 1000).toLocaleString() + "k"; 132 | 133 | } else { 134 | return value; 135 | } 136 | } 137 | } 138 | }], 139 | yAxes: [{ 140 | scaleLabel: { 141 | display: true, 142 | labelString: 'Tx Per Sec' 143 | }, 144 | ticks: { 145 | beginAtZero:true, 146 | min: 0 147 | } 148 | }] 149 | } 150 | } 151 | }); -------------------------------------------------------------------------------- /views/unconfirmed-transactions.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headContent 4 | title Unconfirmed Transactions 5 | 6 | block content 7 | h1(class="h2") Unconfirmed Transactions 8 | hr 9 | 10 | if (false) 11 | pre 12 | code #{JSON.stringify(mempoolDetails.txCount, null, 4)} 13 | 14 | if (mempoolDetails) 15 | - var txCount = mempoolDetails.txCount; 16 | 17 | div(class="card") 18 | div(class="card-header") 19 | span(class="h6") 20 | if (txCount == 1) 21 | span 1 Transaction 22 | else 23 | span #{txCount.toLocaleString()} Transactions 24 | 25 | div(class="card-body") 26 | each tx, txIndex in mempoolDetails.transactions 27 | div(class="xcard mb-3") 28 | div(class="card-header monospace") 29 | if (tx && tx.txid) 30 | strong ##{(txIndex + offset).toLocaleString()} 31 | span – 32 | a(href=("/tx/" + tx.txid)) #{tx.txid} 33 | 34 | div(class="card-body") 35 | - var txInputs = mempoolDetails.txInputsByTransaction[tx.txid]; 36 | - var blockHeight = -1; 37 | 38 | include includes/transaction-io-details.pug 39 | 40 | if (txCount > limit) 41 | - var pageNumber = offset / limit + 1; 42 | - var pageCount = Math.floor(txCount / limit); 43 | - if (pageCount * limit < txCount) { 44 | - pageCount++; 45 | - } 46 | - var paginationUrlFunction = function(x) { 47 | - return paginationBaseUrl + "?limit=" + limit + "&offset=" + ((x - 1) * limit + "&sort=" + sort); 48 | - } 49 | 50 | hr 51 | 52 | include includes/pagination.pug 53 | else 54 | p No unconfirmed transactions found --------------------------------------------------------------------------------