├── .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 |
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 |
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 |
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 |
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
--------------------------------------------------------------------------------