├── public
├── .well-known
│ └── acme-challenge
│ │ └── certbot-challenges-here
├── favicon.ico
├── img
│ ├── qr-btc.png
│ ├── logo
│ │ ├── btc.png
│ │ ├── logo.svg
│ │ └── logo-with-background.svg
│ ├── preview.png
│ ├── screenshots
│ │ ├── block.png
│ │ ├── blocks.png
│ │ ├── homepage.png
│ │ ├── node-details.png
│ │ ├── rpc-browser.png
│ │ ├── transaction.png
│ │ ├── mempool-summary.png
│ │ └── transaction-raw.png
│ ├── network-mainnet
│ │ ├── favicon.ico
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── mstile-150x150.png
│ │ ├── apple-touch-icon.png
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── browserconfig.xml
│ │ ├── site.webmanifest
│ │ ├── coin-icon.svg
│ │ ├── icon.svg
│ │ ├── logo.svg
│ │ ├── safari-pinned-tab.svg
│ │ └── logo-with-background.svg
│ ├── network-regtest
│ │ ├── favicon.ico
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── mstile-150x150.png
│ │ ├── apple-touch-icon.png
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── browserconfig.xml
│ │ ├── site.webmanifest
│ │ ├── coin-icon.svg
│ │ ├── icon.svg
│ │ ├── logo.svg
│ │ └── safari-pinned-tab.svg
│ ├── network-signet
│ │ ├── favicon.ico
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── mstile-150x150.png
│ │ ├── apple-touch-icon.png
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── browserconfig.xml
│ │ ├── site.webmanifest
│ │ ├── coin-icon.svg
│ │ ├── icon.svg
│ │ ├── logo.svg
│ │ └── safari-pinned-tab.svg
│ └── network-testnet
│ │ ├── favicon.ico
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── mstile-150x150.png
│ │ ├── apple-touch-icon.png
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── browserconfig.xml
│ │ ├── site.webmanifest
│ │ ├── coin-icon.svg
│ │ ├── icon.svg
│ │ ├── logo.svg
│ │ └── safari-pinned-tab.svg
├── font
│ ├── bootstrap-icons.woff
│ ├── bootstrap-icons.woff2
│ ├── Ubuntu
│ │ ├── Ubuntu-Bold.woff2
│ │ ├── Ubuntu-Light.woff2
│ │ ├── Ubuntu-Regular.woff2
│ │ └── UFL.txt
│ └── Source_Code_Pro
│ │ ├── SourceCodePro-Bold.woff2
│ │ └── SourceCodePro-Regular.woff2
├── leaflet
│ └── images
│ │ ├── layers.png
│ │ ├── layers-2x.png
│ │ ├── marker-icon.png
│ │ ├── marker-shadow.png
│ │ └── marker-icon-2x.png
├── robots.txt
├── style
│ ├── highlight.min.css
│ └── dataTables.bootstrap4.min.css
├── js
│ ├── chartjs-adapter-moment.min.js
│ ├── dataTables.bootstrap4.min.js
│ └── site.js
├── scss
│ ├── light.scss
│ ├── dark.scss
│ └── main.scss
└── txt
│ └── resource-integrity.json
├── views
├── includes
│ ├── value-display.pug
│ ├── electrum-trust-note.pug
│ ├── tools-card-block.pug
│ ├── debug-overrides.pug
│ ├── time-ago-text.pug
│ ├── page-errors-modal.pug
│ ├── line-graph.pug
│ └── tools-card.pug
├── snippets
│ ├── timestamp.pug
│ ├── quote.pug
│ ├── tz-update-toast.pug
│ ├── index-next-block.pug
│ └── utxo-set.pug
├── changelog.pug
├── api-changelog.pug
├── quote.pug
├── test
│ └── tx-display.pug
├── search.pug
├── admin
│ ├── admin-mixins.pug
│ ├── perf-log.pug
│ ├── app-stats.pug
│ └── os-stats.pug
├── mempool-transactions.pug
├── quotes.pug
├── about.pug
├── connect.pug
├── error.pug
├── blocks.pug
├── bitcoin-whitepaper.pug
├── terminal.pug
├── user-settings.pug
├── tools.pug
├── block-analysis-search.pug
├── api-docs.pug
├── rpc-terminal.pug
├── fun.pug
├── utxo-set.pug
├── projected-blocks-old.pug
├── next-block.pug
├── index.pug
└── layout-iframe.pug
├── raw
└── full-indicator.psd
├── .gitattributes
├── app
├── coins.js
├── auth.js
├── currencies.js
├── redisCache.js
├── credentials.js
├── cacheUtils.js
├── normalizeActions.js
├── appStats.js
├── api
│ ├── blockchairAddressApi.js
│ ├── blockcypherAddressApi.js
│ ├── addressApi.js
│ └── blockchainAddressApi.js
├── systemMonitor.js
├── actionPerformanceMonitor.js
├── statTracker.js
└── sso.js
├── Dockerfile
├── TODO.md
├── bin
├── test.js
├── www
├── frontend-resource-integrity.js
├── refresh-mining-pool-configs.js
└── cli.js
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ └── codeql-analysis.yml
├── CHANGELOG-API.md
├── docs
├── Server-Setup-Docker.md
├── btc-explorer.com.conf
├── explorer.btc21.org.conf
├── nginx-reverse-proxy.md
└── Server-Setup.md
├── LICENSE
├── .gitignore
├── roadmap.md
├── routes
├── testRouter.js
├── snippetRouter.js
└── adminRouter.js
└── package.json
/public/.well-known/acme-challenge/certbot-challenges-here:
--------------------------------------------------------------------------------
1 | certbot
--------------------------------------------------------------------------------
/views/includes/value-display.pug:
--------------------------------------------------------------------------------
1 | include ./shared-mixins.pug
2 |
3 | +valueDisplay(currencyValue)
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/favicon.ico
--------------------------------------------------------------------------------
/public/img/qr-btc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/qr-btc.png
--------------------------------------------------------------------------------
/public/img/logo/btc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/logo/btc.png
--------------------------------------------------------------------------------
/public/img/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/preview.png
--------------------------------------------------------------------------------
/raw/full-indicator.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/raw/full-indicator.psd
--------------------------------------------------------------------------------
/views/snippets/timestamp.pug:
--------------------------------------------------------------------------------
1 | include ../includes/shared-mixins.pug
2 |
3 | +timestamp(timestamp, includeAgo, formatString)
--------------------------------------------------------------------------------
/views/snippets/quote.pug:
--------------------------------------------------------------------------------
1 | extends ../layout-iframe
2 |
3 | block content
4 | .text-center.text-white
5 | +quote(quote, quoteIndex)
--------------------------------------------------------------------------------
/public/font/bootstrap-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/font/bootstrap-icons.woff
--------------------------------------------------------------------------------
/public/font/bootstrap-icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/font/bootstrap-icons.woff2
--------------------------------------------------------------------------------
/public/img/screenshots/block.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/screenshots/block.png
--------------------------------------------------------------------------------
/public/img/screenshots/blocks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/screenshots/blocks.png
--------------------------------------------------------------------------------
/public/leaflet/images/layers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/leaflet/images/layers.png
--------------------------------------------------------------------------------
/public/font/Ubuntu/Ubuntu-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/font/Ubuntu/Ubuntu-Bold.woff2
--------------------------------------------------------------------------------
/public/img/screenshots/homepage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/screenshots/homepage.png
--------------------------------------------------------------------------------
/public/leaflet/images/layers-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/leaflet/images/layers-2x.png
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # https://help.github.com/articles/dealing-with-line-endings/
2 | * text=auto
3 | *.css text eol=lf
4 | *.js text eol=lf
5 |
6 |
--------------------------------------------------------------------------------
/public/font/Ubuntu/Ubuntu-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/font/Ubuntu/Ubuntu-Light.woff2
--------------------------------------------------------------------------------
/public/font/Ubuntu/Ubuntu-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/font/Ubuntu/Ubuntu-Regular.woff2
--------------------------------------------------------------------------------
/public/img/network-mainnet/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-mainnet/favicon.ico
--------------------------------------------------------------------------------
/public/img/network-regtest/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-regtest/favicon.ico
--------------------------------------------------------------------------------
/public/img/network-signet/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-signet/favicon.ico
--------------------------------------------------------------------------------
/public/img/network-testnet/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-testnet/favicon.ico
--------------------------------------------------------------------------------
/public/img/screenshots/node-details.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/screenshots/node-details.png
--------------------------------------------------------------------------------
/public/img/screenshots/rpc-browser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/screenshots/rpc-browser.png
--------------------------------------------------------------------------------
/public/img/screenshots/transaction.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/screenshots/transaction.png
--------------------------------------------------------------------------------
/public/leaflet/images/marker-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/leaflet/images/marker-icon.png
--------------------------------------------------------------------------------
/public/leaflet/images/marker-shadow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/leaflet/images/marker-shadow.png
--------------------------------------------------------------------------------
/app/coins.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const btc = require("./coins/btc.js");
4 |
5 | module.exports = {
6 | "BTC": btc,
7 |
8 | "coins":["BTC"]
9 | };
--------------------------------------------------------------------------------
/public/leaflet/images/marker-icon-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/leaflet/images/marker-icon-2x.png
--------------------------------------------------------------------------------
/public/img/network-mainnet/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-mainnet/favicon-16x16.png
--------------------------------------------------------------------------------
/public/img/network-mainnet/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-mainnet/favicon-32x32.png
--------------------------------------------------------------------------------
/public/img/network-regtest/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-regtest/favicon-16x16.png
--------------------------------------------------------------------------------
/public/img/network-regtest/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-regtest/favicon-32x32.png
--------------------------------------------------------------------------------
/public/img/network-signet/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-signet/favicon-16x16.png
--------------------------------------------------------------------------------
/public/img/network-signet/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-signet/favicon-32x32.png
--------------------------------------------------------------------------------
/public/img/network-signet/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-signet/mstile-150x150.png
--------------------------------------------------------------------------------
/public/img/network-testnet/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-testnet/favicon-16x16.png
--------------------------------------------------------------------------------
/public/img/network-testnet/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-testnet/favicon-32x32.png
--------------------------------------------------------------------------------
/public/img/screenshots/mempool-summary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/screenshots/mempool-summary.png
--------------------------------------------------------------------------------
/public/img/screenshots/transaction-raw.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/screenshots/transaction-raw.png
--------------------------------------------------------------------------------
/public/img/network-mainnet/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-mainnet/mstile-150x150.png
--------------------------------------------------------------------------------
/public/img/network-regtest/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-regtest/mstile-150x150.png
--------------------------------------------------------------------------------
/public/img/network-signet/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-signet/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/img/network-testnet/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-testnet/mstile-150x150.png
--------------------------------------------------------------------------------
/public/img/network-mainnet/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-mainnet/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/img/network-regtest/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-regtest/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/img/network-testnet/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-testnet/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/font/Source_Code_Pro/SourceCodePro-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/font/Source_Code_Pro/SourceCodePro-Bold.woff2
--------------------------------------------------------------------------------
/public/img/network-mainnet/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-mainnet/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/img/network-mainnet/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-mainnet/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/img/network-regtest/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-regtest/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/img/network-regtest/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-regtest/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/img/network-signet/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-signet/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/img/network-signet/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-signet/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/img/network-testnet/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-testnet/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/img/network-testnet/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/img/network-testnet/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/font/Source_Code_Pro/SourceCodePro-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EasyX-Community/btc-rpc-explorer/master/public/font/Source_Code_Pro/SourceCodePro-Regular.woff2
--------------------------------------------------------------------------------
/views/includes/electrum-trust-note.pug:
--------------------------------------------------------------------------------
1 | span
2 | span(data-bs-toggle="tooltip", title="This data is at least partially generated from the Electrum servers currently configured: ")
3 | i.bi-exclamation-triangle.text-warning
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /address/
3 | Disallow: /rpc*
4 | Disallow: /tx-stats
5 | Disallow: /peers
6 | Disallow: /mempool-transactions
7 | Disallow: /block-analysis/
8 | Disallow: /snippet/
9 | Crawl-delay: 7
--------------------------------------------------------------------------------
/views/changelog.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title Changelog
5 |
6 | style.
7 |
8 | block content
9 | +pageTitle("Changelog / Release Notes")
10 |
11 | +contentSection
12 | | !{changelogHtml}
--------------------------------------------------------------------------------
/views/api-changelog.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title API Changelog
5 |
6 | style.
7 |
8 | block content
9 | +pageTitle("API Changelog / Release Notes")
10 |
11 | +contentSection
12 | | !{changelogHtml}
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14 as builder
2 | WORKDIR /workspace
3 | COPY . .
4 | RUN npm install
5 |
6 | FROM node:14-alpine
7 | WORKDIR /workspace
8 | COPY --from=builder /workspace .
9 | RUN apk --update add git
10 | CMD npm start
11 | EXPOSE 3002
12 |
--------------------------------------------------------------------------------
/views/quote.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title #{coinConfig.name} Quote ##{quoteIndex}
5 |
6 | block content
7 | +pageTitle(`${coinConfig.name} Quote #${quoteIndex}`)
8 |
9 | .mt-3.mb-4.px-6
10 | +quote(btcQuotes[quoteIndex])
--------------------------------------------------------------------------------
/views/includes/tools-card-block.pug:
--------------------------------------------------------------------------------
1 | - var siteTool = config.siteTools[toolsItemIndex];
2 | li.mb-2
3 | span(title=siteTool.desc, data-bs-toggle="tooltip")
4 | i.me-1(class=siteTool.iconClass, style="width: 24px;")
5 | a(href=siteTool.url)
6 | span #{siteTool.name}
--------------------------------------------------------------------------------
/public/img/network-mainnet/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #022e70
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/public/img/network-regtest/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/public/img/network-signet/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/public/img/network-testnet/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/views/test/tx-display.pug:
--------------------------------------------------------------------------------
1 | extends ../layout
2 |
3 | block headContent
4 | title Test: Transaction Display
5 |
6 | block content
7 | +pageTitle("Test: Transaction Display")
8 |
9 | +txList(transactions, transactions.length, transactions.length, 0, txInputsByTransaction, {blockHeightsByTxid:blockHeightsByTxid})
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/auth.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const basicAuth = require('basic-auth');
4 |
5 | module.exports = pass => (req, res, next) => {
6 | var cred = basicAuth(req);
7 |
8 | if (cred && cred.pass === pass) {
9 | req.authenticated = true;
10 | return next();
11 | }
12 |
13 | res.set('WWW-Authenticate', `Basic realm="Private Area"`)
14 | .sendStatus(401);
15 | }
16 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | * moment -> luxon :/
2 | * value-validation in /changeSetting (like is currently done for userTzOffset)
3 |
4 | * 24hr comparisons for exchange rate/gold rate?
5 | * /mempool-summary: more granular fee rate bar chart; add line graph on top of bar chart for cumulative blocks
6 | * incoming tx page (live)
7 | * electron: https://gist.github.com/maximilian-lindsey/a446a7ee87838a62099d
8 |
9 | * use flex utilities for "summary rows"?
--------------------------------------------------------------------------------
/views/snippets/tz-update-toast.pug:
--------------------------------------------------------------------------------
1 | .position-fixed.bottom-0.end-0.p-3(style="z-index: 11")
2 | #tzUpdateToast.toast(role="alert" aria-live="assertive" aria-atomic="true")
3 | .toast-header
4 | strong.me-auto Timezone Updated
5 | button.btn-close(type="button", data-bs-dismiss="toast", aria-label="Close")
6 | .toast-body
7 | | Your local timezone was just updated. Refresh the page to see timestamps formatted using your timezone.
--------------------------------------------------------------------------------
/bin/test.js:
--------------------------------------------------------------------------------
1 | const utils = require("../app/utils.js");
2 |
3 | console.log("test");
4 |
5 | global.activeBlockchain = "main";
6 |
7 |
8 |
9 | (async () => {
10 | const perfResults = {};
11 |
12 | await utils.timePromise("abc", async () => {
13 | const x = utils.estimatedSupply(4802177);
14 | console.log("xxx: " + x);
15 |
16 | }, perfResults);
17 |
18 | console.log("perfResults: " + JSON.stringify(perfResults));
19 |
20 | process.exit(0);
21 | })();
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Request a new feature/enhancement be added
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the new feature or enhancement**
11 |
12 | A clear description of the new feature or enhancement. Perhaps a description of how it functions, how it looks, where it "lives", etc.
13 |
14 |
15 | **Additional context**
16 |
17 | Add any other context about the value of the new feature.
18 |
--------------------------------------------------------------------------------
/views/search.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title Search
5 |
6 | block content
7 | +pageTitle("Search")
8 |
9 | +contentSection
10 | form.form.form-inline(method="post", action="./search")
11 | input(type="hidden", name="_csrf", value=csrfToken)
12 |
13 | div.input-group
14 | input.form-control(type="text", name="query", placeholder="block height/hash, txid, address", value=(query), style="width: 400px;")
15 |
16 | button.btn.btn-primary(type="submit") Search
17 |
18 |
--------------------------------------------------------------------------------
/views/includes/debug-overrides.pug:
--------------------------------------------------------------------------------
1 | // debug as if we're in privacy mode (which means we don't have exchange rate data)
2 | //- exchangeRates = null;
3 |
4 | // debug as if we're in performance protection mode (which means we don't calculate UTXO set details)
5 | //- utxoSetSummary = null;
6 | //- utxoSetSummaryPending = false;
7 |
8 | // debug as if we don't have result.blockstats (applies to block pages when node version < 0.17.0)
9 | //if (result)
10 | // - result.blockstats = null;
11 |
12 | // no networkVolume
13 | //- networkVolume = null;
--------------------------------------------------------------------------------
/public/img/network-regtest/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/public/img/network-signet/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/views/includes/time-ago-text.pug:
--------------------------------------------------------------------------------
1 | - var timeAgo = moment.duration(moment.utc(new Date()).diff(moment.utc(new Date(parseInt(timeAgoTime) * 1000))));
2 |
3 | if (timeAgo.asHours() < 1)
4 | if (timeAgo.asMinutes() < 1)
5 | span #{timeAgo.seconds()}s
6 | else
7 | span #{timeAgo.minutes()}m
8 |
9 | else
10 | if (timeAgo.asHours() >= 1 && timeAgo.asHours() < 24)
11 | span #{timeAgo.hours()}h
12 |
13 | if (timeAgo.minutes() > 0)
14 | span #{timeAgo.minutes()}m
15 |
16 | else
17 | span #{utils.shortenTimeDiff(timeAgo.format()).split(", ").join(" ")}
--------------------------------------------------------------------------------
/views/admin/admin-mixins.pug:
--------------------------------------------------------------------------------
1 | mixin adminNav
2 | .card.mb-3.mt-n2.p-0
3 | nav.navbar.navbar-expand.p-1
4 | .container-fluid
5 | .collapse.navbar-collapse
6 | ul.navbar-nav
7 | li.nav-item.me-2.md-md-3
8 | a.nav-link.active(href="./admin/dashboard") Dashboard
9 | li.nav-item.me-2.md-md-3
10 | a.nav-link(href="./admin/app-stats") App Stats
11 | li.nav-item.me-2.md-md-3
12 | a.nav-link(href="./admin/os-stats") OS Stats
13 | li.nav-item.me-2.md-md-3
14 | a.nav-link(href="./admin/perf-log") Performance Log
--------------------------------------------------------------------------------
/public/img/network-mainnet/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "BTC Explorer",
3 | "short_name": "BTC Explorer",
4 | "icons": [
5 | {
6 | "src": "android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#022e70",
17 | "background_color": "#022e70",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/CHANGELOG-API.md:
--------------------------------------------------------------------------------
1 | This changelog specifically tracks changes to the Public API available at `/api` and is maintained separately from the app CHANGELOG such that it can properly adhere to semantic versioning.
2 |
3 | ##### v1.1.0
4 | ###### 2021-12-07
5 |
6 | * Added: /api/blockchain/utxo-set
7 | * Added: /api/address/:address
8 | * Added: /api/mining/next-block
9 | * Added: /api/mining/next-block/txids
10 | * Added: /api/mining/next-block/includes/:txid
11 | * Added: /api/mining/miner-summary
12 |
13 |
14 |
15 | ##### v1.0.0
16 | ###### 2021-08-10
17 |
18 | * Initial release
19 |
--------------------------------------------------------------------------------
/views/mempool-transactions.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title Mempool Transactions
5 |
6 | block content
7 | +pageTitle("Mempool Transactions")
8 |
9 | if (false)
10 | pre
11 | code #{JSON.stringify(transactions, null, 4)}
12 |
13 | if (txCount > 0)
14 | +contentSection(`${txCount.toLocaleString()} Transaction${txCount == 1 ? "" : "s"}`, false, null, false, false)
15 | +txList(transactions, txCount, limit, offset, txInputsByTransaction, {mempoolDetailsByTxid:mempoolDetailsByTxid})
16 |
17 |
18 | else
19 | p No unconfirmed transactions found
20 |
--------------------------------------------------------------------------------
/public/img/network-testnet/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@BitcoinExplorer",
3 | "short_name": "@BitcoinExplorer",
4 | "icons": [
5 | {
6 | "src": "/img/network-mainnet/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/img/network-mainnet/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/views/quotes.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title #{coinConfig.name} Quotes
5 |
6 | block content
7 | +pageTitle(`${coinConfig.name} Quotes`)
8 |
9 |
10 | +dismissableInfoAlert("quotesNoteDismissed", "About Bitcoin Quotes...")
11 | h6.mb-2 About Bitcoin Quotes
12 |
13 | | This is a curated list of quotes that highlight key ideas in Bitcoin and related areas. Suggestions are welcome via an issue or PR on GitHub.
14 |
15 |
16 |
17 | each quote, quoteIndex in btcQuotes
18 | .mt-3.mb-4
19 | +quote(quote, quoteIndex)
--------------------------------------------------------------------------------
/docs/Server-Setup-Docker.md:
--------------------------------------------------------------------------------
1 | ### Setup of https://bitcoinexplorer.org on Ubuntu 20.04
2 |
3 | # update and install packages
4 | apt update
5 | apt upgrade
6 | apt install docker.io
7 |
8 | # get source, npm install
9 | git clone https://github.com/janoside/btc-rpc-explorer.git
10 | cd btc-rpc-explorer
11 |
12 | # build docker image
13 | docker build -t btc-rpc-explorer .
14 |
15 | # run docker image: detached mode, share port 3002, sharing config dir, from the "btc-rpc-explorer" image made above
16 | docker run --name=btc-rpc-explorer -d -v /host-os/env-dir:/container/env-dir --network="host" btc-rpc-explorer
17 |
--------------------------------------------------------------------------------
/public/style/highlight.min.css:
--------------------------------------------------------------------------------
1 | .hljs{display:block;overflow-x:auto;padding:.5em;background:#f0f0f0}.hljs,.hljs-subst{color:#444}.hljs-comment{color:#888}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#bc6060}.hljs-literal{color:#78a960}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta-string{color:#4d99bf}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
--------------------------------------------------------------------------------
/app/currencies.js:
--------------------------------------------------------------------------------
1 | global.currencyTypes = {
2 | "btc": {
3 | id: "btc",
4 | type:"native",
5 | name:"BTC",
6 | multiplier:1,
7 | default:true,
8 | decimalPlaces:8
9 | },
10 | "sat": {
11 | id: "sat",
12 | type:"native",
13 | name:"sat",
14 | multiplier:100000000,
15 | decimalPlaces:0
16 | },
17 | "usd": {
18 | id: "usd",
19 | type:"exchanged",
20 | name:"USD",
21 | multiplier:"usd",
22 | decimalPlaces:2,
23 | symbol:"$"
24 | },
25 | "eur": {
26 | id: "eur",
27 | type:"exchanged",
28 | name:"EUR",
29 | multiplier:"eur",
30 | decimalPlaces:2,
31 | symbol:"€"
32 | },
33 | "gbp": {
34 | id: "gbp",
35 | type:"exchanged",
36 | name:"GBP",
37 | multiplier:"gbp",
38 | decimalPlaces:2,
39 | symbol:"£"
40 | },
41 | };
42 |
43 | global.currencySymbols = {
44 | "btc": "₿",
45 | "usd": "$",
46 | "eur": "€",
47 | "gbp": "£"
48 | };
--------------------------------------------------------------------------------
/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | "use strict";
4 |
5 | const debug = require('debug')('www');
6 | const app = require('../app');
7 |
8 | const v8 = require('v8');
9 | const maxOldSpaceSize = parseInt(process.env.BTCEXP_OLD_SPACE_MAX_SIZE, 10) || 1024;
10 | v8.setFlagsFromString(`--max_old_space_size=${maxOldSpaceSize}`);
11 | debug(`Set max_old_space_size to ${maxOldSpaceSize} MB`);
12 |
13 | app.set('port', process.env.PORT || process.env.BTCEXP_PORT || 3002);
14 | app.set('host', process.env.BTCEXP_HOST || '127.0.0.1');
15 |
16 | const server = app.listen(app.get('port'), app.get('host'), () => {
17 | debug('Express server starting on ' + server.address().address + ':' + server.address().port);
18 |
19 | if (app.onStartup) {
20 | (async function() {
21 | await app.onStartup();
22 |
23 | })();
24 |
25 | }
26 |
27 | debug('Express server startup complete.');
28 | });
29 |
--------------------------------------------------------------------------------
/views/about.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title About
5 |
6 | block content
7 | h1.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 your own node. This tool is easy to run but lacks some features compared to database-backed explorers.
11 |
12 | p I built this tool because I wanted to use it myself. Whatever reasons one might have for running a full node (trustlessness, technical curiosity, supporting the network, etc) it's helpful to appreciate the "fullness" of your own node. With this explorer, you can not only explore the blockchain (in the traditional sense of the term "explorer"), but also explore the functional capabilities of your own node.
13 |
14 | p Pull requests are welcome!
15 | a(href="https://github.com/janoside/btc-rpc-explorer") github.com/janoside/btc-rpc-explorer
16 |
--------------------------------------------------------------------------------
/views/connect.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | +pageTitle("RPC Connect")
5 |
6 | form(method="post", action="./connect")
7 | input(type="hidden", name="_csrf", value=csrfToken)
8 |
9 | .mb-3
10 | label(for="input-host") Host / IP
11 | input.form-control(id="input-host", type="text", name="host", placeholder="Host / IP", value=host)
12 |
13 | .mb-3
14 | label(for="input-port") Port
15 | input.form-control(id="input-port", type="text", name="port", placeholder="Port", value=port)
16 |
17 | .mb-3
18 | label(for="input-username") Username
19 | input.form-control(id="input-username", type="text", name="username", placeholder="Username", value=username)
20 |
21 | .mb-3
22 | label(for="input-password") Password
23 | input.form-control(id="input-password", type="password", name="password", placeholder="Password")
24 |
25 | .mb-3
26 | input.btn.btn-primary.w-100(type="submit" value="Connect")
27 |
28 |
--------------------------------------------------------------------------------
/public/img/network-regtest/coin-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/img/network-mainnet/coin-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/img/network-signet/coin-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/img/network-testnet/coin-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/btc-explorer.com.conf:
--------------------------------------------------------------------------------
1 | ## http://domain.com redirects to https://domain.com
2 | server {
3 | server_name explorer.btc21.org;
4 | listen 80;
5 | #listen [::]:80 ipv6only=on;
6 |
7 | location / {
8 | return 301 https://explorer.btc21.org$request_uri;
9 | }
10 | }
11 |
12 | ## Serves httpS://domain.com
13 | server {
14 | server_name explorer.btc21.org;
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 |
--------------------------------------------------------------------------------
/docs/explorer.btc21.org.conf:
--------------------------------------------------------------------------------
1 | ## http://domain.com redirects to https://domain.com
2 | server {
3 | server_name explorer.btc21.org;
4 | listen 80;
5 | #listen [::]:80 ipv6only=on;
6 |
7 | location / {
8 | return 301 https://explorer.btc21.org$request_uri;
9 | }
10 | }
11 |
12 | ## Serves httpS://domain.com
13 | server {
14 | server_name explorer.btc21.org;
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 |
--------------------------------------------------------------------------------
/views/error.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | if (errorType)
5 | if (errorType == "noRpcConnection")
6 | +pageTitle("No RPC Connection")
7 |
8 | else
9 | +pageTitle(errorType)
10 |
11 | else
12 | +pageTitle("Error")
13 |
14 |
15 | if (errorType == "noRpcConnection")
16 | +warningAlert
17 | .mb-2 This explorer currently is failing to connect to your Bitcoin Core node.
18 | .mb-2 Check your connection details (host & port for Bitcoin Core), as well as your authentication details (username, password, etc).
19 | .mb-0 All of these parameters need to be specified in a ".env" file or via commandline parameters. See the project homepage to review how to configure this explorer.
20 |
21 | else if (message)
22 | +contentSection
23 | | #{message}
24 |
25 | else
26 | p Unknown error
27 |
28 | if (error && error.stack)
29 | +contentSection(error.status ? `Status: ${error.status}` : null)
30 | pre
31 | code.json #{error.stack}
32 |
--------------------------------------------------------------------------------
/views/admin/perf-log.pug:
--------------------------------------------------------------------------------
1 | extends ../layout
2 |
3 | include ./admin-mixins.pug
4 |
5 | block headContent
6 | title Performance Log
7 |
8 | block content
9 | +adminNav
10 |
11 |
12 | +pageTitle("Performance Log")
13 |
14 | table.table.table-striped
15 | thead
16 | tr
17 | th #
18 | th ID
19 | th Type
20 | th Date
21 |
22 | tbody
23 | each item, itemIndex in perfLog
24 | tr(xclass=(itemIndex % 2 == 0 ? "bg-dark" : false))
25 | td #{item.index.toLocaleString()}
26 | td #{item.id}
27 | td #{item.action}
28 | td
29 | | -#{moment.duration(new Date().getTime() - item.date.getTime()).format()}
30 |
31 | //- var timeDiff = moment.duration(moment.utc(new Date(parseInt(block.time) * 1000)).diff(moment.utc(new Date(parseInt(blocks[blockIndex - 1].time) * 1000))));
32 |
33 | //include ../includes/time-ago-text.pug
34 |
35 | tr(xclass=(itemIndex % 2 == 0 ? "bg-dark" : false))
36 | td(colspan="3")
37 | pre #{JSON.stringify(item.results, null, 4)}
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/img/network-regtest/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/img/network-signet/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/img/network-mainnet/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/img/network-testnet/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bin/frontend-resource-integrity.js:
--------------------------------------------------------------------------------
1 | const crypto = require("crypto");
2 | const fs = require("fs");
3 | const path = require("path");
4 |
5 | const dirs = [
6 | "public/js/",
7 | "public/style/",
8 | "public/leaflet/",
9 | "public/font/",
10 | ];
11 |
12 | const filetypeMatch = /^.*\.(js|css)$/;
13 |
14 | const hashesByFilename = {};
15 |
16 | dirs.forEach(dirPath => {
17 | console.log("\nDirectory: " + dirPath);
18 |
19 | fs.readdirSync(path.join(process.cwd(), dirPath)).forEach(file => {
20 | if (file.match(filetypeMatch)) {
21 | var content = fs.readFileSync(path.join(dirPath, file));
22 |
23 | var hash = crypto.createHash("sha384");
24 |
25 | data = hash.update(content, 'utf-8');
26 |
27 | gen_hash = data.digest('base64');
28 |
29 | console.log("\t" + file + " -> " + gen_hash);
30 |
31 | hashesByFilename[file] = `sha384-${gen_hash}`;
32 | }
33 | });
34 | });
35 |
36 | fs.writeFileSync(path.join(process.cwd(), "public/txt/resource-integrity.json"), JSON.stringify(hashesByFilename, null, 4));
37 |
38 | console.log("\npublic/txt/resource-integrity.json written.\n");
--------------------------------------------------------------------------------
/views/blocks.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title Blocks
5 |
6 | block content
7 | .clearfix
8 | .float-start
9 | +pageTitle("Blocks")
10 | .float-end
11 | if (blocks)
12 | nav(aria-label="Page navigation")
13 | ul.pagination.justify-content-center.mt-3
14 |
15 | li.page-item(class=(sort == "desc" ? "active" : false))
16 | a.page-link(href=(sort == "desc" ? "javascript:void(0)" : `./blocks?limit=${limit}&offset=0&sort=desc`))
17 | span(aria-hidden="true") Newest blocks first
18 |
19 | li.page-item(class=(sort == "asc" ? "active" : false))
20 | a.page-link(href=(sort == "asc" ? "javascript:void(0)" : `./blocks?limit=${limit}&offset=0&sort=asc`))
21 | span(aria-hidden="true") Oldest blocks first
22 |
23 | if (blocks)
24 | +contentSection
25 | include includes/blocks-list.pug
26 |
27 | if (blockCount > limit)
28 | if (blocks.length % 2 == 1)
29 | hr.mt-4
30 | else
31 | .mb-2
32 |
33 | .mt-4
34 | +pagination(limit, offset, sort, blockCount, paginationBaseUrl, "center", true)
35 |
36 | else
37 | p No blocks found
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-2020 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 |
--------------------------------------------------------------------------------
/public/img/logo/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.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 |
60 | # cache dir
61 | cache/
62 |
63 | *.css.map
64 |
--------------------------------------------------------------------------------
/public/img/network-regtest/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/img/network-signet/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/img/network-mainnet/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/img/network-testnet/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/views/bitcoin-whitepaper.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title Bitcoin Whitepaper
5 |
6 | block content
7 | +pageTitle("Bitcoin Whitepaper", "(Extracted from the blockchain!)")
8 |
9 | +dismissableInfoAlert("whitepaperPageNoteDismissed", "About the Bitcoin Whitepaper Tool...")
10 | p Below is the Bitcoin whitepaper, extracted from data in the Bitcoin blockchain (transaction
11 | a(href="./tx/54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713") 54e48e5f5c6…
12 | span.ms-2 to be precise), served by your Bitcoin Core node.
13 |
14 | div The data has been decoded by this tool and is displayed in an iframe below. You can view it directly and/or download it at
15 | a(href="./bitcoin.pdf") /bitcoin.pdf
16 | |.
17 |
18 | if (global.activeBlockchain == "main")
19 | iframe(src="./bitcoin.pdf", title="The Bitcoin Whitepaper", style="width: 100%;", height="800")
20 |
21 | else
22 | +warningAlert
23 | .mb-2 Whoops! The Bitcoin Whitepaper is embedded in a particular transaction in the mainnet blockchain. It looks like this node is configured for a different network, so the data is not available to be extracted.
24 | | Try running a mainnet node to use this tool.
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 |
12 | A clear and concise description of what the bug is.
13 |
14 | **Environment (please complete the following information):**
15 |
16 | - Bitcoin Core / Node Version [e.g. 0.16.3]
17 | - NodeJS Version [e.g. 9.x]
18 | - Browser [e.g. chrome, safari]
19 | - Code Version / Commit [e.g. ab6cde8]
20 | - Installation Method [e.g. "npm" or "source code"]
21 |
22 | **Configuration file content**
23 |
24 | Please include the content from the following files. **BE SURE TO MODIFY YOUR CREDENTIALS BEFORE SUBMITTING!!!**
25 | - bitcoin.conf
26 | - Your btc-rpc-explorer environment configuration (either `$WORKING_DIR/.env` or `~/.config/btc-rpc-explorer.env`)
27 |
28 | **To Reproduce**
29 |
30 | Steps to reproduce the behavior:
31 | 1. Go to '...'
32 | 2. Click on '....'
33 | 3. Scroll down to '....'
34 | 4. See error
35 |
36 | **Screenshots or Log Output**
37 |
38 | If applicable, add screenshots or log output to help explain your problem.
39 |
40 | **Additional context**
41 |
42 | Add any other context about the problem here.
43 |
--------------------------------------------------------------------------------
/views/snippets/index-next-block.pug:
--------------------------------------------------------------------------------
1 | include ../includes/shared-mixins.pug
2 |
3 |
4 |
5 | +summaryRow(1)
6 | +summaryItem
7 |
8 |
9 | +numWithMutedDecimals(new Decimal(minFeeRate).toDP(2).toString())
10 | span.mx-1 ‐
11 |
12 | if (maxFeeRate - minFeeRate > 10)
13 | +numWithMutedDecimals(new Decimal(maxFeeRate).toDP(0).toString())
14 |
15 | else
16 | +numWithMutedDecimals(new Decimal(maxFeeRate).toDP(2).toString())
17 |
18 | span.text-tiny.text-muted.ms-1 sat/vB
19 |
20 | br
21 |
22 |
23 |
24 |
25 |
26 |
27 | | #{txCount.toLocaleString()}
28 | span.text-tiny.text-muted.ms-1 tx
29 | span.mx-2.text-muted /
30 |
31 | - var full = new Decimal(totalWeight).dividedBy(coinConfig.maxBlockWeight).times(100);
32 | - var full2 = full.toDP(0);
33 |
34 |
35 | if (full >= 99 || full2 == 99)
36 | span.text-success.small.border-dotted(title="The predicted next block is full.", data-bs-toggle="tooltip")
37 | | Full
38 | i.bi-check2.ms-1
39 |
40 | else
41 | span.text-primary.small.border-dotted(title=`The predicted next block is ~${full2}% full.`, data-bs-toggle="tooltip")
42 | | #{full2}%
43 |
44 |
45 |
46 | span.mx-2.text-muted /
47 |
48 | span.small
49 | span.me-1.border-dotted(title="Σ fees", data-bs-toggle="tooltip") Σ
50 | +valueDisplay(totalFees, {hideLessSignificantDigits:true})
51 |
52 |
53 |
--------------------------------------------------------------------------------
/docs/nginx-reverse-proxy.md:
--------------------------------------------------------------------------------
1 | Instructions for nginx reverse proxy, accessible via https (thanks [@leshacat](https://github.com/leshacat))
2 |
3 | * `sudo apt -y install nginx-full python-certbot-nginx`
4 | * Edit `/etc/nginx/sites-available/default`
5 |
6 | Leave the default config, scroll to the bottom, paste in at bottom and edit:
7 | ```
8 | upstream explorer-servers {
9 | ip_hash;
10 | server srv1.example.com:3000 max_fails=1 weight=4;
11 | server srv2.example.com:3000 max_fails=1 weight=2;
12 | server srv3.example.com:3000 max_fails=1 weight=1;
13 | }
14 |
15 | server {
16 | server_name explorer.example.com; # managed by Certbot
17 |
18 | proxy_set_header Host $host;
19 | proxy_set_header X-Real-IP $remote_addr;
20 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
21 | proxy_set_header X-Forwarded-Proto $scheme;
22 | proxy_set_header X-Forwarded-Ssl on;
23 |
24 | location / {
25 |
26 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
27 | proxy_set_header Host $host;
28 |
29 | proxy_pass http://explorer-servers;
30 |
31 | proxy_http_version 1.1;
32 | proxy_set_header Upgrade $http_upgrade;
33 | proxy_set_header Connection "upgrade";
34 |
35 | listen 80 default_server;
36 | listen [::]:80 default_server;
37 |
38 | }
39 | ```
40 |
41 | * `systemctl enable nginx`
42 | * `systemctl restart nginx`
43 | * `certbot --nginx -d explorer.example.com`
44 |
--------------------------------------------------------------------------------
/views/includes/page-errors-modal.pug:
--------------------------------------------------------------------------------
1 | div.modal.fade(id="pageErrorsModal" role="dialog" aria-hidden="true")
2 | div.modal-dialog.modal-xl(role="document")
3 | div.modal-content
4 | div.modal-header
5 | h5.modal-title Page Errors
6 |
7 | button.close(type="button" data-bs-dismiss="modal" aria-label="Close")
8 | span(aria-hidden="true") ×
9 |
10 | div.modal-body
11 | if (false)
12 | pre
13 | code.json #{JSON.stringify(pageErrors, null, 4)}
14 |
15 | if (true)
16 | each item, itemIndex in pageErrors
17 | div(class=(itemIndex < (pageErrors.length - 1) ? "mb-3" : false))
18 | h6 Error ##{(itemIndex + 1).toLocaleString()}
19 | hr
20 | //pre
21 | // code.json #{JSON.stringify(item.error, null, 4)}
22 |
23 | pre
24 | code.json #{JSON.stringify(item, null, 4)}
25 |
26 | if (item.error.userData)
27 | div.mb-3
28 | h4.h6 Error Data
29 |
30 | div.highlight
31 | pre
32 | code.json #{JSON.stringify(item.error.userData, null, 4)}
33 |
34 | if (item.error.stack)
35 | h6 Stacktrace
36 | - var stackFirstNLines = item.error.stack.split("\n").slice(0, 7).join("\n");
37 | div.highlight
38 | pre
39 | code.json #{stackFirstNLines}
40 |
41 |
42 |
43 | div.modal-footer
44 | button.btn.btn-secondary(type="button" data-bs-dismiss="modal") Close
45 |
--------------------------------------------------------------------------------
/public/js/chartjs-adapter-moment.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * chartjs-adapter-moment v1.0.0
3 | * https://www.chartjs.org
4 | * (c) 2021 chartjs-adapter-moment Contributors
5 | * Released under the MIT license
6 | */
7 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("moment"),require("chart.js")):"function"==typeof define&&define.amd?define(["moment","chart.js"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).moment,e.Chart)}(this,(function(e,t){"use strict";function n(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var f=n(e);const a={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};t._adapters._date.override("function"==typeof f.default?{_id:"moment",formats:function(){return a},parse:function(e,t){return"string"==typeof e&&"string"==typeof t?e=f.default(e,t):e instanceof f.default||(e=f.default(e)),e.isValid()?e.valueOf():null},format:function(e,t){return f.default(e).format(t)},add:function(e,t,n){return f.default(e).add(t,n).valueOf()},diff:function(e,t,n){return f.default(e).diff(f.default(t),n)},startOf:function(e,t,n){return e=f.default(e),"isoWeek"===t?(n=Math.trunc(Math.min(Math.max(0,n),6)),e.isoWeekday(n).startOf("day").valueOf()):e.startOf(t).valueOf()},endOf:function(e,t){return f.default(e).endOf(t).valueOf()}}:{})}));
8 | //# sourceMappingURL=chartjs-adapter-moment.min.js.map
9 |
--------------------------------------------------------------------------------
/app/redisCache.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const redis = require("redis");
4 | const bluebird = require("bluebird");
5 |
6 | const config = require("./config.js");
7 | const utils = require("./utils.js");
8 |
9 | let redisClient = null;
10 | if (config.redisUrl) {
11 | bluebird.promisifyAll(redis.RedisClient.prototype);
12 |
13 | redisClient = redis.createClient({url:config.redisUrl});
14 | }
15 |
16 | function createCache(keyPrefix, onCacheEvent) {
17 | return {
18 | get: function(key) {
19 | const prefixedKey = `${keyPrefix}-${key}`;
20 |
21 | return new Promise(function(resolve, reject) {
22 | onCacheEvent("redis", "try", prefixedKey);
23 |
24 | redisClient.getAsync(prefixedKey).then(function(result) {
25 | if (result == null) {
26 | onCacheEvent("redis", "miss", prefixedKey);
27 |
28 | resolve(null);
29 |
30 | } else {
31 | onCacheEvent("redis", "hit", prefixedKey);
32 |
33 | resolve(JSON.parse(result));
34 | }
35 | }).catch(function(err) {
36 | onCacheEvent("redis", "error", prefixedKey);
37 |
38 | utils.logError("328rhwefghsdgsdss", err);
39 |
40 | reject(err);
41 | });
42 | });
43 | },
44 | set: function(key, obj, maxAgeMillis) {
45 | const prefixedKey = `${keyPrefix}-${key}`;
46 |
47 | redisClient.set(prefixedKey, JSON.stringify(obj), "PX", maxAgeMillis);
48 | }
49 | };
50 | }
51 |
52 | module.exports = {
53 | active: (redisClient != null),
54 | createCache: createCache
55 | }
--------------------------------------------------------------------------------
/views/terminal.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title Terminal
5 |
6 | block content
7 | +pageTitle("Terminal")
8 |
9 | +dismissableInfoAlert
10 | .mb-3 This is an internal developer tool.
11 |
12 | +contentSection
13 | form(id="terminal-form")
14 | .mb-3
15 | label(for="input-cmd") Command
16 | input.form-control(type="text", id="input-cmd", name="cmd")
17 |
18 | input.btn.btn-primary.w-100(type="submit", value="Send")
19 |
20 |
21 | hr
22 |
23 | div(id="terminal-output")
24 |
25 | block endOfBody
26 | script.
27 | var csrfToken = $('meta[name=csrf-token]').attr('content');
28 |
29 | $(document).ready(function() {
30 | $("#terminal-form").submit(function(e) {
31 | e.preventDefault();
32 |
33 | var cmd = $("#input-cmd").val()
34 |
35 | var postData = {};
36 | postData.cmd = cmd;
37 | postData._csrf = csrfToken;
38 |
39 | $.post(
40 | "/terminal",
41 | postData,
42 | function(response, textStatus, jqXHR) {
43 | var t = new Date().getTime();
44 |
45 | ("#terminal-output").prepend("
" + cmd + "
" + response + "
");
46 | console.log(response);
47 |
48 | $("#output-" + t + " pre code").each(function(i, block) {
49 | hljs.highlightBlock(block);
50 | });
51 |
52 | return false;
53 | })
54 | .done(function(data) {
55 | });
56 |
57 | return false;
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/public/img/network-mainnet/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/img/network-regtest/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/credentials.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const os = require('os');
4 | const path = require('path');
5 | const url = require('url');
6 |
7 | const btcUri = process.env.BTCEXP_BITCOIND_URI ? url.parse(process.env.BTCEXP_BITCOIND_URI, true) : { query: { } };
8 | const btcAuth = btcUri.auth ? btcUri.auth.split(':') : [];
9 |
10 | module.exports = {
11 | rpc: {
12 | host: btcUri.hostname || process.env.BTCEXP_BITCOIND_HOST || "127.0.0.1",
13 | port: btcUri.port || process.env.BTCEXP_BITCOIND_PORT || 8332,
14 | username: btcAuth[0] || process.env.BTCEXP_BITCOIND_USER,
15 | password: btcAuth[1] || process.env.BTCEXP_BITCOIND_PASS,
16 | cookie: btcUri.query.cookie || process.env.BTCEXP_BITCOIND_COOKIE || path.join(os.homedir(), '.bitcoin', '.cookie'),
17 | timeout: parseInt(btcUri.query.timeout || process.env.BTCEXP_BITCOIND_RPC_TIMEOUT || 5000),
18 | },
19 |
20 | // optional: enter your api access key from ipstack.com below
21 | // to include a map of the estimated locations of your node's
22 | // peers
23 | // format: "ID_FROM_IPSTACK"
24 | ipStackComApiAccessKey: process.env.BTCEXP_IPSTACK_APIKEY,
25 |
26 | // optional: enter your api access key from mapbox.com below
27 | // to enable the tiles for map of the estimated locations of
28 | // your node's peers
29 | // format: "APIKEY_FROM_MAPBOX"
30 | mapBoxComApiAccessKey: process.env.BTCEXP_MAPBOX_APIKEY,
31 |
32 | // optional: GA tracking code
33 | // format: "UA-..."
34 | googleAnalyticsTrackingId: process.env.BTCEXP_GANALYTICS_TRACKING,
35 |
36 | // optional: sentry.io error-tracking url
37 | // format: "SENTRY_IO_URL"
38 | sentryUrl: process.env.BTCEXP_SENTRY_URL,
39 | };
40 |
--------------------------------------------------------------------------------
/public/img/logo/logo-with-background.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/img/network-mainnet/logo-with-background.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/scss/light.scss:
--------------------------------------------------------------------------------
1 | $body-bg: #fff;
2 | $body-color: #212529;
3 |
4 | $main-bg: #f0f4f7;
5 |
6 | $border-color: #e4e7eb;
7 | $card-border-color: $border-color;
8 | $card-highlight-color: #567997;
9 |
10 |
11 | $nav-tabs-border-color: #adb5bd;
12 | $nav-tabs-link-active-color: lighten($body-color, 5%);
13 | $nav-tabs-link-active-border-color: #adb5bd #adb5bd $body-bg !important;
14 |
15 |
16 | $alert-bg-scale: -70%;
17 | $alert-border-scale: -60%;
18 |
19 |
20 |
21 |
22 | $table-striped-bg: rgba(0, 0, 0, 0.04);
23 |
24 |
25 | @import "./main";
26 |
27 |
28 |
29 |
30 |
31 | .bg-main {
32 | background-color: $main-bg !important;
33 | }
34 |
35 | .bg-header-footer {
36 | background-color: #212529 !important;
37 | }
38 |
39 | .bg-header-footer-highlight {
40 | background-color: lighten(#212529, 15%) !important;
41 | }
42 |
43 | .bg-header-footer {
44 | background-color: #162740 !important;
45 | }
46 |
47 | .bg-header-footer-highlight {
48 | background-color: lighten(#162740, 15%) !important;
49 | }
50 |
51 | .bg-gradient-body-to-main {
52 | background: linear-gradient(0deg, $main-bg 0%, $body-bg 100%);
53 | }
54 |
55 | .bg-card-highlight-badge {
56 | background-color: darken($card-bg, 10%) !important;
57 | }
58 |
59 | .bg-tx-separator {
60 | background-color: #dce1e5;
61 | }
62 |
63 | .border-card-highlight-badge {
64 | border-color: darken($card-bg, 16%) !important;
65 | }
66 |
67 | .text-card-highlight {
68 | color: $card-highlight-color !important;
69 | }
70 |
71 | .border-dotted {
72 | border-bottom: dotted 1px #979ca5;
73 | }
74 |
75 | .card-highlight {
76 | background-color: darken($card-bg, 3%);
77 | border: solid 1px darken($card-bg, 10%) !important;
78 | color: $body-color;
79 | }
--------------------------------------------------------------------------------
/views/user-settings.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title User Settings
5 |
6 | block content
7 | +pageTitle("User Settings")
8 |
9 | +contentSection("Display Options")
10 | .row.pb-3.border-bottom.mb-4
11 | .col.text-start
12 | h6 Hide info notes
13 | span.text-muted Enabling this option hides all informational notes across the site.
14 | .col.text-end.me-3
15 | .d-inline-block.text-center
16 | if (userSettings.hideInfoNotes && userSettings.hideInfoNotes == "true")
17 | div Yes
18 | a(href=`./changeSetting?name=hideInfoNotes&value=false`)
19 | i.bi-toggle2-on.fs-3
20 |
21 | else
22 | div No
23 | a(href=`./changeSetting?name=hideInfoNotes&value=true`)
24 | i.bi-toggle2-off.fs-3
25 |
26 |
27 | .row
28 | .col.text-start
29 | h6 Manual UTC Offset
30 | span.small.text-muted.ms-1 (hours)
31 | span.text-muted If you want to set a custom UTC offset, rather than using your browser's default, you can set it here. This is helpful if your browser uses UTC time as a privacy-protecting measure. For locales ~West of UTC (i.e. West of London), set a negative value; for locales ~East of UTC, set a positive value.
32 | .col.text-end.me-3
33 | .d-inline-block.text-end
34 | form(method="get", action="./changeSetting")
35 | input(type="hidden", name="name", value="userTzOffset")
36 | input(type="text", name="value", value=userTzOffset)
37 |
38 | span.small.text-muted (Example: for New York, enter "-5")
39 |
40 | +contentSection("User Settings")
41 | pre
42 | code.json #{JSON.stringify(userSettings, null, 4)}
43 |
44 | hr
45 |
46 | a.btn.btn-primary(href="./admin/resetUserSettings") Reset To Defaults
--------------------------------------------------------------------------------
/views/tools.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title Tools
5 |
6 | mixin toolSections(sectionsData)
7 | .row
8 | each indexList in sectionsData
9 | .col
10 | ul.list-unstyled.mb-3
11 | each sectionIndex in indexList
12 | - var section = config.site.toolSections[sectionIndex];
13 |
14 | +contentSection(section.name)
15 |
16 | each itemIndex, itemIndexIndex in section.items
17 | - var item = config.siteTools[itemIndex];
18 |
19 | if (itemIndexIndex > 0)
20 | hr
21 |
22 | .clearfix
23 | .float-start.pt-1(style="width: 38px;")
24 | i.fs-5(class=item.iconClass, style="width: 20px; margin-right: 10px;")
25 |
26 | .float-start
27 | div
28 | a(href=item.url) #{item.name}
29 |
30 | div.mt-n1(style="padding-left: 39px;")
31 | p #{item.desc}
32 |
33 |
34 |
35 |
36 | block content
37 | +pageTitle("Tools")
38 |
39 |
40 | div
41 | //- var sections1Col = config.site.toolSections.filter(x => true);// utils.splitArrayIntoChunksByChunkCount(priorityList, 1);
42 | //- var indexLists2Col = utils.splitArrayIntoChunksByChunkCount(priorityList, 2);
43 | - var sectionIndexes3Col = [[0, 1], [2], [3, 4]];//config.site.toolSections.filter(x => true); utils.splitArrayIntoChunksByChunkCount(priorityList, 3);
44 |
45 | // xs
46 | if (true)
47 | div.d-block.d-sm-none(id="tools-1-col")
48 | +toolSections([[0, 1, 2, 3, 4]])
49 |
50 | // sm, md, lg
51 | div.d-none.d-sm-block.d-xl-none(id="tools-2-col")
52 | +toolSections([[0, 1, 4], [2, 3]])
53 |
54 |
55 | // xl, xxl
56 | div.d-none.d-xl-block(id="tools-3-col")
57 | +toolSections([[0, 1], [2], [3, 4]])
58 |
59 |
--------------------------------------------------------------------------------
/app/cacheUtils.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const LRU = require("lru-cache");
4 |
5 | function createMemoryLruCache(cacheObj, onCacheEvent) {
6 | return {
7 | get: (key) => {
8 | return new Promise((resolve, reject) => {
9 | onCacheEvent("memory", "try", key);
10 |
11 | var val = cacheObj.get(key);
12 |
13 | if (val != null) {
14 | onCacheEvent("memory", "hit", key);
15 |
16 | } else {
17 | onCacheEvent("memory", "miss", key);
18 | }
19 |
20 | resolve(cacheObj.get(key));
21 | });
22 | },
23 | set: (key, obj, maxAge) => {
24 | cacheObj.set(key, obj, maxAge);
25 |
26 | onCacheEvent("memory", "set", key);
27 | },
28 | del: (key) => {
29 | cacheObj.del(key);
30 |
31 | onCacheEvent("memory", "del", key);
32 | }
33 | }
34 | }
35 |
36 | function tryCache(cacheKey, cacheObjs, index, resolve, reject) {
37 | if (index == cacheObjs.length) {
38 | resolve(null);
39 |
40 | return;
41 | }
42 |
43 | cacheObjs[index].get(cacheKey).then((result) => {
44 | if (result != null) {
45 | resolve(result);
46 |
47 | } else {
48 | tryCache(cacheKey, cacheObjs, index + 1, resolve, reject);
49 | }
50 | });
51 | }
52 |
53 | function createTieredCache(cacheObjs) {
54 | return {
55 | get:(key) => {
56 | return new Promise((resolve, reject) => {
57 | tryCache(key, cacheObjs, 0, resolve, reject);
58 | });
59 | },
60 | set:(key, obj, maxAge) => {
61 | for (var i = 0; i < cacheObjs.length; i++) {
62 | cacheObjs[i].set(key, obj, maxAge);
63 | }
64 | }
65 | }
66 | }
67 |
68 | function lruCache(size) {
69 | return new LRU(size);
70 | }
71 |
72 | module.exports = {
73 | lruCache: lruCache,
74 | createMemoryLruCache: createMemoryLruCache,
75 | createTieredCache: createTieredCache
76 | }
--------------------------------------------------------------------------------
/views/block-analysis-search.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title Block Analysis
5 |
6 | block content
7 | +pageTitle("Block Analysis")
8 |
9 | div.card.shadow-sm.mb-huge
10 | div.card-body
11 | h6.mb-3 Search for a block by height or hash to see a summary analysis of the transactions within that block.
12 |
13 | div.mb-3
14 | form.form.form-inline(method="get", action="./block-analysis")
15 | input(type="hidden", name="_csrf", value=csrfToken)
16 |
17 | div.input-group
18 | input.form-control(id="input-value", type="text", name="query", placeholder="block height/hash", value=(query), style="width: 400px;")
19 |
20 | button.btn.btn-primary(type="submit", aria-label="Go") Go
21 |
22 | hr.my-4
23 |
24 | if (global.prunedBlockchain)
25 | div.alert.alert-primary.pb-0(role="alert")
26 | h6.mb-2 Note About Pruning
27 | - var msgMarkdown = `Blockchain \`pruning\` is enabled on your node. This setting tells your node that after validating transactions it may discard data that is non-essential for future validation needs.\n\nThe current \`prune height\` for your node is ${global.pruneHeight.toLocaleString()}, and a block analysis will fail for any block height earlier than that.`;
28 | | !{markdown(msgMarkdown)}
29 |
30 | else
31 |
32 | h6 Selection of example blocks:
33 |
34 | - var heights = [0, 170, 100000, 210000, 420000, 481824];
35 | ul
36 | each height in heights
37 | li
38 | a(href=`./block-analysis/${height}`) Block ##{height.toLocaleString()}
39 |
40 | block endOfBody
41 | script.
42 | $(document).ready(function() {
43 | $("form").submit(function() {
44 | window.location.href = `./block-analysis/${$("#input-value").val()}`;
45 |
46 | return false;
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/views/api-docs.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title API Docs
5 |
6 | block content
7 | +pageTitle("API Docs")
8 | div.fs-4.mt-n3.mb-3 Current Version:
9 | a.ms-2(href="./api/version", title="View version API call: /api/version", data-bs-toggle="tooltip", target="_blank") v#{apiDocs.version}
10 | small.ms-2 (
11 | a(href="./api/changelog") changelog
12 | | )
13 |
14 | if (false)
15 | pre
16 | code.json #{JSON.stringify(categories, null, 4)}
17 |
18 |
19 | +dismissableInfoAlert("apiDocsNoteDismissed", "About the API...")
20 | h6.mb-2 About the API
21 |
22 | | The API documented below is made available by btc-rpc-explorer. The actions are organized by category. From this documentation you can directly click on an action's link to see the output format in your browser.
23 |
24 |
25 | each cat, catIndex in categories
26 | h3.h5.mb-1.fw-light.text-capitalize #{cat.name}
27 | +contentSection
28 | each item, itemIndex in cat.items
29 | if (itemIndex > 0)
30 | hr
31 |
32 | .row.p-2
33 | .col-md-3
34 | a(href=(item.testUrl ? `.${item.testUrl}` : `.${item.url}`), target="_blank") #{item.url}
35 |
36 | .col-md-1
37 | span(title=`Return type: ${item.returnType}`, data-bs-toggle="tooltip")
38 | +lightBadge(item.returnType)
39 |
40 | .col-md-8
41 |
42 | div #{item.desc}
43 |
44 | if (item.optionalParams)
45 | .mt-2
46 | .mb-1
47 | small.me-2 Optional parameters
48 |
49 | each x, xName in item.optionalParams
50 | div
51 | +lightBadge(xName)
52 | span.small #{x}
53 |
54 | if (false && item.example)
55 | .mt-3
56 | span.text-muted Example output:
57 | pre
58 | code.json #{JSON.stringify(item.example)}
59 |
--------------------------------------------------------------------------------
/app/normalizeActions.js:
--------------------------------------------------------------------------------
1 | const buildNormalizingRegexes = (baseUrl) => {
2 | return [
3 | { regex: new RegExp(`^${baseUrl}$`, "i"), action:"index" },
4 | { regex: new RegExp(`^${baseUrl}block\-height\/.*`, "i"), action: "block-height" },
5 | { regex: new RegExp(`^${baseUrl}block\/.*`, "i"), action: "block-hash" },
6 | { regex: new RegExp(`^${baseUrl}block\-analysis\/.*`, "i"), action: "block-analysis" },
7 | { regex: new RegExp(`^${baseUrl}tx\/.*`, "i"), action: "transaction" },
8 | { regex: new RegExp(`^${baseUrl}address\/.*`, "i"), action: "address" },
9 |
10 | { regex: new RegExp(`^${baseUrl}api/blocks-by-height\/.*`, "i"), action: "api.blocks-by-height" },
11 | { regex: new RegExp(`^${baseUrl}api/block-headers-by-height\/.*`, "i"), action: "api.block-headers-by-height" },
12 | { regex: new RegExp(`^${baseUrl}api/block-stats-by-height\/.*`, "i"), action: "api.block-stats-by-height" },
13 | { regex: new RegExp(`^${baseUrl}api/mempool-txs\/.*`, "i"), action: "api.mempool-txs" },
14 | { regex: new RegExp(`^${baseUrl}api/raw-tx-with-inputs\/.*`, "i"), action: "api.raw-tx-with-inputs" },
15 | { regex: new RegExp(`^${baseUrl}api/block-tx-summaries\/.*`, "i"), action: "api.block-tx-summaries" },
16 | { regex: new RegExp(`^${baseUrl}api/utils\/.*`, "i"), action: "api.utils-func" },
17 |
18 | { regex: new RegExp(`^${baseUrl}admin/dashboard`, "i"), action: "admin.dashboard" },
19 | ];
20 | }
21 |
22 | module.exports = (baseUrl, action) => {
23 | const normalizingRegexes = buildNormalizingRegexes(baseUrl);
24 |
25 | for (let i = 0; i < normalizingRegexes.length; i++) {
26 | if (normalizingRegexes[i].regex.test(action)) {
27 | return normalizingRegexes[i].action;
28 | }
29 | }
30 |
31 | if (action.startsWith(baseUrl)) {
32 | return action.substring(baseUrl.length);
33 | }
34 |
35 | return action;
36 | };
--------------------------------------------------------------------------------
/public/txt/resource-integrity.json:
--------------------------------------------------------------------------------
1 | {
2 | "bootstrap.bundle.min.js": "sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p",
3 | "chart.min.js": "sha384-ovAl4wbIXnEbY76lSb6GrprFBkoYeu4RKYYNMsADThIn2AweWPOyJqoyR/8/kgML",
4 | "chartjs-adapter-moment.min.js": "sha384-Z9r2EsEmivx0l8T8TvYoqqGcpO0cCjKbqVXB8tYUa0hIWKtGVl0TmaF263CjS6XR",
5 | "dataTables.bootstrap4.min.js": "sha384-uiSTMvD1kcI19sAHJDVf68medP9HA2E2PzGis9Efmfsdb8p9+mvbQNgFhzii1MEX",
6 | "decimal.js": "sha384-AZER8B64Ei3MdcUsKj9o83PHYCWb4dY9wJz58HzSDLat+G/QlGUdXhIlNOH56LUe",
7 | "highlight.pack.js": "sha384-OGoFdvlhYqw3L+BFpHxdz5136RO9tUlt7OZ2qQZ0N6Z9Qqx0rCQBsg9ko7X4vC64",
8 | "jquery.dataTables.min.js": "sha384-rgWRqC0OFPisxlUvl332tiM/qmaNxnlY46eksSZD84t+s2vZlqGeHrncwIRX7CGp",
9 | "jquery.min.js": "sha384-vtXRMe3mGCbOeY7l30aIg8H9p3GdeSe4IFlP6G8JMa7o7lXvnz3GFKzPxzJdPfGK",
10 | "moment.min.js": "sha384-CJyhAlbbRZX14Q8KxKBt0na1ad4KBs9PklAiNk2Efxs9sgimbIZm9kYLJQeNMUfM",
11 | "sentry.min.js": "sha384-da/Bo2Ah6Uw3mlhl6VINMblg2SyGbSnULKrukse3P5D9PTJi4np9HoKvR19D7zOL",
12 | "site.js": "sha384-RNCa5sCPh1hkJyKAu8z5Hai8r9IxpVBF2W6GFhSb2JSuSlECW8d85ufzyXBSRwLZ",
13 | "bootstrap-icons.css": "sha384-F2hGAg3VbKKichOp5qwbhbe1e56ymDT49EQWyH5rqCb0qNewcTpB+wJ9Ayrw7tKQ",
14 | "dark.css": "sha384-cbyBi/NO0/dmZ3u3CIAl6fgTWzV4mbneKcZkYuTdwuipj4DYou1zkCpAzp75XY/0",
15 | "dataTables.bootstrap4.min.css": "sha384-EkHEUZ6lErauT712zSr0DZ2uuCmi3DoQj6ecNdHQXpMpFNGAQ48WjfXCE5n20W+R",
16 | "highlight.min.css": "sha384-s4RLYRjGGbVqKOyMGGwfxUTMOO6D7r2eom7hWZQ6BjK2Df4ZyfzLXEkonSm0KLIQ",
17 | "light.css": "sha384-3CgKfbPgdn/RUkPGKfYkSy7jgoxqcupuIUhfcQGN0tFNCPMtk7O9wS2xjaaRewTQ",
18 | "leaflet.css": "sha384-6wKUKNzA6h/S6gZ1lWQppeGaVXvK1AUAsEznGBghzlEu1fNcxJGYVRiroSHr+OwU",
19 | "leaflet.js": "sha384-RFZC58YeKApoNsIbBxf4z6JJXmh+geBSgkCQXFyh+4tiFSJmJBt+2FbjxW7Ar16M"
20 | }
--------------------------------------------------------------------------------
/app/appStats.js:
--------------------------------------------------------------------------------
1 | const statNames = [
2 | "process.cpu",
3 | "process.mem_mb",
4 | "mem.heap.used",
5 | "mem.heap.limit",
6 | "os.loadavg.1min",
7 | "os.loadavg.5min"
8 | ];
9 |
10 | const dataPointsToKeep = 60;
11 | const downsamplesToKeep = 72;
12 | const dataPointsPerDownsample = 6;
13 | const appStats = {};
14 | const downsampledAppStats = {};
15 |
16 | const trackAppStats = (name, stats) => {
17 | if (statNames.includes(name)) {
18 | if (!appStats[name]) {
19 | appStats[name] = [];
20 | downsampledAppStats[name] = [];
21 | }
22 |
23 | dataset = appStats[name];
24 |
25 | if (stats.max) {
26 | dataset.push({time:new Date().getTime(), value: stats.max});
27 | }
28 |
29 | if (dataset.length > (dataPointsToKeep + dataPointsPerDownsample)) {
30 | var downsamplePoints = dataset.slice(0, dataPointsPerDownsample);
31 | var max = -Infinity;
32 |
33 | // find max of downsample
34 | downsamplePoints.forEach(x => { if (x.value > max) { max = x.value; } });
35 |
36 | downsampledAppStats[name].push({time:downsamplePoints[0].time, value:max});
37 |
38 | while (dataset.length > dataPointsToKeep) {
39 | dataset.shift();
40 | }
41 | }
42 |
43 | while (downsampledAppStats[name].length > downsamplesToKeep) {
44 | downsampledAppStats[name].shift();
45 | }
46 | }
47 | };
48 |
49 | const getAllAppStats = () => {
50 | var allStats = {};
51 |
52 | if (appStats[statNames[0]]) {
53 | for (var i = 0; i < statNames.length; i++) {
54 | if (downsampledAppStats[statNames[i]]) {
55 | allStats[statNames[i]] = downsampledAppStats[statNames[i]].concat(appStats[statNames[i]]);
56 |
57 | } else {
58 | allStats[statNames[i]] = appStats[statNames[i]];
59 | }
60 | }
61 | }
62 |
63 | return allStats;
64 | };
65 |
66 | module.exports = {
67 | trackAppStats: trackAppStats,
68 | statNames: statNames,
69 | getAllAppStats: getAllAppStats
70 | };
71 |
72 |
--------------------------------------------------------------------------------
/views/includes/line-graph.pug:
--------------------------------------------------------------------------------
1 | +graphPageScriptSetup
2 |
3 | canvas.mb-3(id=graphData.id)
4 |
5 | script.
6 | Chart.defaults.elements.point.radius = 1;
7 | var ctx = document.getElementById("#{graphData.id}").getContext('2d');
8 | var graph = new Chart(ctx, {
9 | type: 'line',
10 | labels: [#{graphData.labels}],
11 | data: {
12 | datasets: [{
13 | borderColor: '#007bff',
14 | borderWidth: 2,
15 | backgroundColor: 'rgba(0,0,0,0)',
16 | data: #{graphData.dataVar},
17 | }]
18 | },
19 | options: {
20 | animation:{
21 | duration:0
22 | },
23 | title: {
24 | display: false,
25 | text: '#{graphData.title}'
26 | },
27 | plugins: {
28 | legend: {
29 | display: false
30 | },
31 | },
32 | scales: {
33 | x: {
34 | type: 'linear',
35 | position: 'bottom',
36 | scaleLabel: {
37 | display: true,
38 | labelString: '#{graphData.xaxisTitle}'
39 | },
40 | grid: {
41 | color: gridLineColor
42 | },
43 | ticks: {
44 | stepSize: #{graphData.xaxisStep},
45 | /*callback: function(value, index, values) {
46 | if (value > 1000000) {
47 | return (value / 1000000).toLocaleString() + "M";
48 |
49 | } else if (value > 1000) {
50 | return (value / 1000).toLocaleString() + "k";
51 |
52 | } else {
53 | return value;
54 | }
55 | }*/
56 | }
57 | },
58 | y: {
59 | scaleLabel: {
60 | display: true,
61 | labelString: '#{graphData.yaxisTitle}'
62 | },
63 | grid: {
64 | color: gridLineColor
65 | },
66 | ticks: {
67 | callback: function(value, index, values) {
68 | if (value > 1000000) {
69 | return (value / 1000000).toLocaleString() + "M";
70 |
71 | } else {
72 | return value;
73 | }
74 | }
75 | }
76 | }
77 | }
78 | }
79 | });
80 |
--------------------------------------------------------------------------------
/app/api/blockchairAddressApi.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const axios = require("axios");
4 | const utils = require("./../utils.js");
5 |
6 |
7 | function getAddressDetails(address, scriptPubkey, sort, limit, offset) {
8 | // Note: blockchair api seems to not respect the limit parameter, always using 100
9 | return new Promise(async (resolve, reject) => {
10 | var mainnetUrl = `https://api.blockchair.com/bitcoin/dashboards/address/${address}/?offset=${offset}`;
11 | var testnetUrl = `https://api.blockchair.com/bitcoin/testnet/dashboards/address/${address}/?offset=${offset}`;
12 | var url = (global.activeBlockchain == "main") ? mainnetUrl : ((global.activeBlockchain == "test") ? testnetUrl : mainnetUrl);
13 |
14 | var options = {
15 | url: url,
16 | headers: {
17 | 'User-Agent': 'request'
18 | }
19 | };
20 |
21 | try {
22 | const response = await axios.get(
23 | url,
24 | { headers: { "User-Agent": "axios" }});
25 |
26 | var responseObj = response.data;
27 | responseObj = responseObj.data[address];
28 |
29 | var result = {};
30 |
31 | result.txids = [];
32 |
33 | // blockchair doesn't support offset for paging, so simulate up to the hard cap of 2,000
34 | for (var i = 0; i < Math.min(responseObj.transactions.length, limit); i++) {
35 | var txid = responseObj.transactions[i];
36 |
37 | result.txids.push(txid);
38 | }
39 |
40 | result.txCount = responseObj.address.transaction_count;
41 | result.totalReceivedSat = responseObj.address.received;
42 | result.totalSentSat = responseObj.address.spent;
43 | result.balanceSat = responseObj.address.balance;
44 | result.source = "blockchair.com";
45 |
46 | resolve({addressDetails:result});
47 |
48 | } catch (err) {
49 | utils.logError("308dhew3w83", err);
50 |
51 | reject(err);
52 | }
53 | });
54 | }
55 |
56 |
57 | module.exports = {
58 | getAddressDetails: getAddressDetails
59 | };
--------------------------------------------------------------------------------
/roadmap.md:
--------------------------------------------------------------------------------
1 | * Privacy analysis
2 | * Multisig designations for tx inputs
3 | * Ref1: https://mempool.space/tx/09195e5c88b620b3c9d55f628edd5115fbfbdf49576579a6e2ed329e0e9bcf73
4 | * Src: https://github.com/mempool/mempool/blob/master/frontend/src/app/components/address-labels/address-labels.component.ts#L33
5 | * Ref2: https://blockstream.info/tx/09195e5c88b620b3c9d55f628edd5115fbfbdf49576579a6e2ed329e0e9bcf73?expand
6 | * Src: https://github.com/Blockstream/electrs/blob/new-index/src/rest.rs#L223
7 | * Use RPC "decodescript"?
8 | * Also include this in /block-analysis summaries
9 | * Holidays
10 | * Genesis Day
11 | * Pizza Day
12 | * https://twitter.com/nvk/status/1463857031551541260
13 | * Countdown to halving
14 | * Countdown to difficulty change
15 | * Historical mining config:
16 | * Hal: 78?
17 | * Script parsing
18 |
19 | #### Misc / Minor
20 |
21 | * cleanup trailing whitespace: https://github.com/janoside/btc-rpc-explorer/commit/abccbcced24a3299b559166f8c4b58a33f9008d0#comments
22 |
23 |
24 | * "utils.js" accessible from frontend JS code (to avoid some of /snippet?)
25 |
26 | * move to simpler variable structure - remove "result.getblock" kind of structure in favor of "block"
27 | * don't double-get the block for /block-height pages (maybe /block pages too): in action handler "getBlockByHeight" is called, then "getBlockByHashWithTransactions", which internally calls "getBlockByHash"
28 | * get rid of magic numbers (e.g. 100,000,000)
29 | * re-visit the old "conflicted results" concept in electrumAddressApi (it's been removed from UI when moving to v3, but maybe should return)
30 |
31 | * cache difficulty data on /diff-hist page, so subsequent runs are super fast (tiny amt of data to cache)
32 | * cache miner data on /mining-summary page, so subsequent runs are super fast (tiny amt of data to cache)
33 |
34 |
35 | * UTXO status on outputs on all txLists (transaction page is done, need to add block page, address page, test/tx-list page)
36 |
--------------------------------------------------------------------------------
/views/rpc-terminal.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title RPC Terminal
5 |
6 | block content
7 | +pageTitle("RPC Terminal")
8 |
9 |
10 | if (!config.demoSite && (!config.credentials.rpc || !config.credentials.rpc.rpc))
11 | .mb-2
12 | a.btn.btn-secondary(href="./disconnect") Disconnect from node
13 |
14 |
15 | +dismissableInfoAlert("rpcTermPageNoteDismissed", "About RPC Terminal...")
16 | .mb-2 This tool lets you send custom RPC commands to your node and will display the results below.
17 | .mb-2 To browse all available RPC commands you can use the RPC Browser.
18 |
19 |
20 | div.card.shadow-sm.mb-3
21 | div.card-body
22 | form(id="terminal-form")
23 | .mb-3
24 | label(for="input-cmd") Command
25 | input.form-control(type="text", id="input-cmd", name="cmd")
26 |
27 | input.btn.btn-primary.w-100(type="submit", value="Send")
28 |
29 | hr
30 |
31 | div(id="terminal-output")
32 |
33 | block endOfBody
34 | script.
35 | var csrfToken = $('meta[name=csrf-token]').attr('content');
36 |
37 | $(document).ready(function() {
38 | $("#terminal-form").submit(function(e) {
39 | e.preventDefault();
40 |
41 | var cmd = $("#input-cmd").val()
42 |
43 | var postData = {};
44 | postData.cmd = cmd;
45 | postData._csrf = csrfToken;
46 |
47 | $.post(
48 | "./rpc-terminal",
49 | postData,
50 | function(response, textStatus, jqXHR) {
51 | var t = new Date().getTime();
52 |
53 | $("#terminal-output").prepend("" + cmd + "
" + response + "
");
54 | console.log(response);
55 |
56 | $("#output-" + t + " pre code").each(function(i, block) {
57 | hljs.highlightBlock(block);
58 | });
59 |
60 | return false;
61 | })
62 | .done(function(data) {
63 | });
64 |
65 | return false;
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/public/js/dataTables.bootstrap4.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | DataTables Bootstrap 4 integration
3 | ©2011-2017 SpryMedia Ltd - datatables.net/license
4 | */
5 | (function(b){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return b(a,window,document)}):"object"===typeof exports?module.exports=function(a,d){a||(a=window);if(!d||!d.fn.dataTable)d=require("datatables.net")(a,d).$;return b(d,a,a.document)}:b(jQuery,window,document)})(function(b,a,d,m){var f=b.fn.dataTable;b.extend(!0,f.defaults,{dom:"<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>><'row'<'col-sm-12'tr>><'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
6 | renderer:"bootstrap"});b.extend(f.ext.classes,{sWrapper:"dataTables_wrapper dt-bootstrap4",sFilterInput:"form-control form-control-sm",sLengthSelect:"custom-select custom-select-sm form-control form-control-sm",sProcessing:"dataTables_processing card",sPageButton:"paginate_button page-item"});f.ext.renderer.pageButton.bootstrap=function(a,h,r,s,j,n){var o=new f.Api(a),t=a.oClasses,k=a.oLanguage.oPaginate,u=a.oLanguage.oAria.paginate||{},e,g,p=0,q=function(d,f){var l,h,i,c,m=function(a){a.preventDefault();
7 | !b(a.currentTarget).hasClass("disabled")&&o.page()!=a.data.action&&o.page(a.data.action).draw("page")};l=0;for(h=f.length;l",
8 | {"class":t.sPageButton+" "+g,id:0===r&&"string"===typeof c?a.sTableId+"_"+c:null}).append(b("",{href:"#","aria-controls":a.sTableId,"aria-label":u[c],"data-dt-idx":p,tabindex:a.iTabIndex,"class":"page-link"}).html(e)).appendTo(d),a.oApi._fnBindAction(i,{action:c},m),p++)}},i;try{i=b(h).find(d.activeElement).data("dt-idx")}catch(v){}q(b(h).empty().html('').children("ul"),s);i!==m&&b(h).find("[data-dt-idx="+i+"]").focus()};return f});
9 |
--------------------------------------------------------------------------------
/app/api/blockcypherAddressApi.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const axios = require("axios");
4 | const utils = require("./../utils.js");
5 |
6 |
7 | function getAddressDetails(address, scriptPubkey, sort, limit, offset) {
8 | return new Promise(async (resolve, reject) => {
9 | if (address.startsWith("bc1")) {
10 | reject({userText:"blockcypher.com API does not support bc1 (native Segwit) addresses"});
11 |
12 | return;
13 | }
14 |
15 | var limitOffset = limit + offset;
16 | var mainnetUrl = `https://api.blockcypher.com/v1/btc/main/addrs/${address}?limit=${limitOffset}`;
17 | var testnetUrl = `https://api.blockcypher.com/v1/btc/test3/addrs/${address}?limit=${limitOffset}`;
18 | var url = (global.activeBlockchain == "main") ? mainnetUrl : ((global.activeBlockchain == "test") ? testnetUrl : mainnetUrl);
19 |
20 | var options = {
21 | url: url,
22 | headers: {
23 | 'User-Agent': 'request'
24 | }
25 | };
26 |
27 | try {
28 | const apiResponse = await axios.get(
29 | url,
30 | { headers: { "User-Agent": "axios" }});
31 |
32 | var blockcypherJson = apiResponse.data;
33 |
34 | var response = {};
35 |
36 | response.txids = [];
37 | response.blockHeightsByTxid = {};
38 |
39 | // blockcypher doesn't support offset for paging, so simulate up to the hard cap of 2,000
40 | for (var i = offset; i < Math.min(blockcypherJson.txrefs.length, limitOffset); i++) {
41 | var tx = blockcypherJson.txrefs[i];
42 |
43 | response.txids.push(tx.tx_hash);
44 | response.blockHeightsByTxid[tx.tx_hash] = tx.block_height;
45 | }
46 |
47 | response.txCount = blockcypherJson.n_tx;
48 | response.totalReceivedSat = blockcypherJson.total_received;
49 | response.totalSentSat = blockcypherJson.total_sent;
50 | response.balanceSat = blockcypherJson.final_balance;
51 | response.source = "blockcypher.com";
52 |
53 | resolve({addressDetails:response});
54 |
55 | } catch (err) {
56 | utils.logError("097wef0adsgadgs", err);
57 |
58 | reject(err);
59 | }
60 | });
61 | }
62 |
63 |
64 | module.exports = {
65 | getAddressDetails: getAddressDetails
66 | };
--------------------------------------------------------------------------------
/docs/Server-Setup.md:
--------------------------------------------------------------------------------
1 | ### Setup of https://bitcoinexplorer.org on Ubuntu 20.04
2 |
3 | Update and install packages
4 |
5 | apt update
6 | apt upgrade
7 | apt install git nginx gcc g++ make python3-certbot-nginx
8 |
9 | Install NVM from https://github.com/nvm-sh/nvm
10 |
11 | nvm ls-remote
12 |
13 | # install latest node from output of ls-remote above, e.g.:
14 | nvm install 15.13.0
15 |
16 | npm install -g pm2
17 |
18 | Misc setup
19 |
20 | # add user for btc-related stuff
21 | adduser bitcoin # leave everything blank if you want
22 |
23 | # gen self-signed cert
24 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/selfsigned.key -out /etc/ssl/certs/selfsigned.crt
25 |
26 | # get nginx config
27 | wget https://raw.githubusercontent.com/janoside/btc-rpc-explorer/master/docs/explorer.btc21.org.conf
28 | mv explorer.btc21.org.conf /etc/nginx/sites-available/bitcoinexplorer.org
29 |
30 | Get source, npm install
31 |
32 | cd /home/bitcoin
33 | git clone https://github.com/janoside/btc-rpc-explorer.git
34 | cd /home/bitcoin/btc-rpc-explorer
35 | npm install
36 |
37 | # startup via pm2
38 | pm2 start bin/www --name "btc"
39 |
40 | # get letsencrypt cert
41 | certbot --nginx -d bitcoinexplorer.org
42 |
43 | Tor setup
44 |
45 | apt install tor
46 |
47 | Edit /etc/tor/torrc
48 |
49 | 1. Uncomment `ControlPort 9051`
50 | 2. Uncomment `CookieAuthentication 1`
51 | 3. If applicable, add Torv3 Hidden service credentials to `/var/lib/tor/btcexp...onion`
52 | * chmod 700 for directory, owned by the same "tor" user as other files in that dir
53 | * chmod 600 for the files in the "btcexp...onion" dir)
54 | 5. Add `HiddenServiceDir /var/lib/tor/btcexp...onion/`
55 | 6. Add `HiddenServicePort 80 127.0.0.1:3000`
56 |
57 |
58 | Tor startup
59 |
60 | service tor start
61 |
62 | # verify tor startup
63 | ps -ef | grep tor
64 |
65 | # verify tor listening on 9050 (proxy) and 9051 (control port)
66 | netstat -nlp | grep 905
67 |
68 |
--------------------------------------------------------------------------------
/views/fun.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title #{coinConfig.name} Fun
5 |
6 | block content
7 | +pageTitle(`${coinConfig.name} Fun`)
8 |
9 |
10 | +dismissableInfoAlert("funPageNoteDismissed", "About Bitcoin Fun...")
11 | .mb-2 This is a curated list of fun and interesting things in the blockchain or related to the underlying data:
12 | ul
13 | li Historical firsts
14 | li Technical quirks and oddities
15 | li Cultural references
16 |
17 | .mb-0 Help curating this list is welcome! You can submit new items by opening an issue or PR on Github.
18 |
19 |
20 | +contentSection
21 | .table-responsive
22 | table.table.table-borderless.table-striped
23 | thead
24 | tr
25 | th.text-card-highlight.text-uppercase.fw-light Date
26 | th.text-card-highlight.text-uppercase.fw-light Item
27 | th.text-card-highlight.text-uppercase.fw-light Reference
28 | tbody
29 | each item, index in coinConfig.historicalData
30 | if (item.chain == activeBlockchain)
31 | tr
32 | td #{item.date}
33 |
34 | if (true)
35 | td
36 | if (item.type == "tx")
37 | a(href=`./tx/${item.txid}@${item.blockHeight}`) #{item.summary}
38 | else if (item.type == "block")
39 | a(href=`./block/${item.blockHash}`) #{item.summary}
40 | else if (item.type == "blockheight")
41 | a(href=`./block/${item.blockHash}`) #{item.summary}
42 | else if (item.type == "address")
43 | a(href=`./address/${item.address}`) #{item.summary}
44 | else if (item.type == "link")
45 | a(href=item.url) #{item.summary}
46 |
47 | td
48 | if (item.referenceUrl && item.referenceUrl.trim().length > 0)
49 | - var matches = item.referenceUrl.match(/^https?\:\/\/([^\/:?#]+)(?:[\/:?#]|$)/i);
50 |
51 | - var domain = null;
52 | - var domain = matches && matches[1];
53 |
54 | if (domain)
55 | a(href=item.referenceUrl, rel="nofollow") #{domain}
56 | i.bi-box-arrow-up-right
57 | else
58 | a(href=item.referenceUrl, rel="nofollow") Reference
59 | else
60 | span -
61 |
--------------------------------------------------------------------------------
/app/systemMonitor.js:
--------------------------------------------------------------------------------
1 | const os = require("os");
2 | const v8 = require("v8");
3 | const pidusage = require("pidusage");
4 | const statTracker = require("./statTracker.js");
5 | const debugLog = require("debug")("systemMonitor");
6 |
7 | try {
8 | var eventLoopStats = require("event-loop-stats");
9 |
10 | } catch (err) {
11 | debugLog("Failed loading event-loop-stats, skipping system monitor");
12 | }
13 |
14 | const systemMonitorInterval = setInterval(() => {
15 | pidusage(process.pid, (err, stat) => {
16 | if (err) {
17 | debugLog(err);
18 |
19 | return;
20 | }
21 |
22 | debugLog("pidusage: " + JSON.stringify(stat));
23 |
24 | statTracker.trackValue("process.cpu", stat.cpu);
25 | statTracker.trackValue("process.mem_mb", stat.memory / 1024 / 1024);
26 | statTracker.trackValue("process.ctime", stat.ctime);
27 | statTracker.trackValue("process.uptime_s", stat.elapsed / 1000);
28 |
29 | let loadavg = os.loadavg();
30 |
31 | statTracker.trackValue("os.loadavg.1min", loadavg[0]);
32 | statTracker.trackValue("os.loadavg.5min", loadavg[1]);
33 | statTracker.trackValue("os.loadavg.15min", loadavg[2]);
34 |
35 | let heapStats = v8.getHeapStatistics();
36 |
37 | statTracker.trackValue("mem.heap.total", heapStats.total_heap_size / 1024 / 1024);
38 | statTracker.trackValue("mem.heap.total-executable", heapStats.total_heap_size_executable / 1024 / 1024);
39 | statTracker.trackValue("mem.heap.total-physical", heapStats.total_physical_size / 1024 / 1024);
40 | statTracker.trackValue("mem.heap.total-available", heapStats.total_available_size / 1024 / 1024);
41 | statTracker.trackValue("mem.heap.used", heapStats.used_heap_size / 1024 / 1024);
42 | statTracker.trackValue("mem.heap.limit", heapStats.heap_size_limit / 1024 / 1024);
43 | statTracker.trackValue("mem.malloced", heapStats.malloced_memory / 1024 / 1024);
44 | statTracker.trackValue("mem.malloced-peak", heapStats.peak_malloced_memory / 1024 / 1024);
45 |
46 | if (eventLoopStats) {
47 | let loopStats = eventLoopStats.sense();
48 |
49 | statTracker.trackValue("eventloop.min", loopStats.min);
50 | statTracker.trackValue("eventloop.max", loopStats.max);
51 | statTracker.trackValue("eventloop.sum", loopStats.sum);
52 | statTracker.trackValue("eventloop.num", loopStats.num);
53 | }
54 | });
55 | }, process.env.SYSTEM_MONITOR_INTERVAL || 60 * 60 * 1000);
56 |
57 | systemMonitorInterval.unref();
--------------------------------------------------------------------------------
/routes/testRouter.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const debug = require("debug");
4 | const debugLog = debug("btcexp:router");
5 |
6 | const express = require('express');
7 | const csurf = require('csurf');
8 | const router = express.Router();
9 | const util = require('util');
10 | const moment = require('moment');
11 | const bitcoinCore = require("btc-rpc-client");
12 | const qrcode = require('qrcode');
13 | const bitcoinjs = require('bitcoinjs-lib');
14 | const bip32 = require('bip32');
15 | const bs58check = require('bs58check');
16 | const { bech32, bech32m } = require("bech32");
17 | const sha256 = require("crypto-js/sha256");
18 | const hexEnc = require("crypto-js/enc-hex");
19 | const Decimal = require("decimal.js");
20 | const semver = require("semver");
21 | const markdown = require("markdown-it")();
22 | const asyncHandler = require("express-async-handler");
23 |
24 | const utils = require('./../app/utils.js');
25 | const coins = require("./../app/coins.js");
26 | const config = require("./../app/config.js");
27 | const coreApi = require("./../app/api/coreApi.js");
28 | const addressApi = require("./../app/api/addressApi.js");
29 | const rpcApi = require("./../app/api/rpcApi.js");
30 | const btcQuotes = require("./../app/coins/btcQuotes.js");
31 |
32 |
33 | router.get("/tx-display", asyncHandler(async (req, res, next) => {
34 | res.locals.transactions = [];
35 | res.locals.txInputsByTransaction = {};
36 | res.locals.blockHeightsByTxid = {};
37 |
38 | var txidOrder = [];
39 |
40 | const promises = [];
41 | for (const [txid, data] of Object.entries(global.coinConfig.testData.txDisplayTestList)) {
42 | txidOrder.push(txid);
43 |
44 | const blockHash = data.blockHash;
45 |
46 | res.locals.blockHeightsByTxid[txid] = data.blockHeight;
47 |
48 | promises.push(utils.timePromise("test.tx-display.getRawTransactionsWithInputs", async () => {
49 | const transactionData = await coreApi.getRawTransactionsWithInputs([txid], 5, blockHash);
50 |
51 | res.locals.transactions.push(transactionData.transactions[0]);
52 |
53 | for (const [resultTxid, resultData] of Object.entries(transactionData.txInputsByTransaction)) {
54 | res.locals.txInputsByTransaction[resultTxid] = resultData;
55 | }
56 | //console.log(JSON.stringify(transactionData.txInputsByTransaction));
57 | }));
58 | }
59 |
60 | res.locals.maxTxOutputDisplayCount = 12;
61 |
62 | // todo: include a random mempool tx
63 |
64 | await Promise.all(promises);
65 |
66 | res.locals.transactions.sort((a, b) => {
67 | return txidOrder.indexOf(a.txid) - txidOrder.indexOf(b.txid);
68 | });
69 |
70 | res.render("test/tx-display.pug");
71 |
72 | next();
73 | }));
74 |
75 | module.exports = router;
76 |
--------------------------------------------------------------------------------
/views/utxo-set.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title UTXO Set
5 |
6 | block content
7 | +pageTitle("UTXO Set", "\"Run the Numbers\"")
8 |
9 | +dismissableInfoAlert("utxoSetPageNoteDismissed", "About the UTXO Set...")
10 | .mb-2 A UTXO, or Unspent Transaction Output, defines a spendable unit of BTC. Every UTXO, when spent, is effectively destroyed and replaced with multiple other UTXOs of different values (see simplified example below), a process analogous to smelting and re-forging physical coins each time they are spent (impractical in the physical world, but easy when working with bits of data).
11 |
12 | .my-4
13 | .d-block.d-md-none.text-center
14 | .badge.bg-light.text-dark UTXO #1 (1 BTC)
15 | div ↓
16 | .badge.bg-danger spend (destroy)
17 | div ↓
18 | div
19 | .badge.bg-light.text-dark UTXO #2 (0.25 BTC)
20 | br
21 | .badge.bg-light.text-dark UTXO #3 (0.75 BTC)
22 |
23 | .d-none.d-md-block.text-center.mb-3
24 | .d-flex.justify-content-center
25 | div
26 | span.badge.bg-light.text-dark.border UTXO #1 (1 BTC)
27 | div
28 | span.mx-2 →
29 | .badge.bg-danger spend (destroy)
30 | span.mx-2 →
31 | div
32 | span.badge.bg-light.text-dark UTXO #2 (0.25 BTC)
33 | br
34 | span.badge.bg-light.text-dark UTXO #3 (0.75 BTC)
35 |
36 | .mb-2 With this in mind, the UTXO Set is the set of all UTXOs and defines all spendable BTC units.
37 | .mb-2 Every BTC node is capable of independently and trustlessly verifying the entire UTXO Set! Your node has done (or is currently doing) that verification.
38 |
39 | | (Note that the verification process can be quite slow, depending on the node's hardware configuration and indexing options used.)
40 |
41 | #utxo-set-content
42 | .spinner-border.spinner-border-sm
43 |
44 | block endOfBody
45 | script.
46 | $(document).ready(function() {
47 | $.get("./snippet/utxo-set", function(data) {
48 | $("#utxo-set-content").html(data);
49 |
50 | // enable tooltips everywhere
51 | var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
52 | var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
53 | return new bootstrap.Tooltip(tooltipTriggerEl);
54 | });
55 |
56 | // enable popovers everywhere
57 | var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
58 | var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
59 | return new bootstrap.Popover(popoverTriggerEl);
60 | });
61 |
62 | hljs.highlightAll();
63 | });
64 | });
--------------------------------------------------------------------------------
/app/actionPerformanceMonitor.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const onHeaders = require('on-headers');
4 | const os = require('os');
5 | const v8 = require('v8');
6 | const debug = require("debug");
7 | const debugLog = debug("monitor");
8 | const utils = require("./utils.js");
9 |
10 |
11 | const onHeadersListener = (config, req, statusCode, startTimeNanos, statTracker) => {
12 | try {
13 | const responseTimeNanos = process.hrtime.bigint() - startTimeNanos;
14 | const responseTimeMillis = parseInt(responseTimeNanos) * 1e-6;
15 |
16 | const category = Math.floor(statusCode / 100);
17 |
18 |
19 | let action = req.baseUrl + req.path;
20 |
21 | if (config.ignoredEndsWithActionsRegex.test(action)) {
22 | return;
23 | }
24 |
25 | if (config.ignoredStartsWithActionsRegex.test(action)) {
26 | return;
27 | }
28 |
29 | let allActions = "*";
30 | if (config.normalizeAction) {
31 | action = config.normalizeAction(action);
32 | allActions = config.normalizeAction(allActions);
33 | }
34 |
35 | statTracker.trackPerformance(`action.${action}`, responseTimeMillis);
36 | statTracker.trackPerformance("action.*", responseTimeMillis);
37 |
38 | statTracker.trackEvent(`action-status.${action}.${category}00`);
39 | statTracker.trackEvent(`action-status.*.${category}00`);
40 |
41 | var userAgent = req.headers['user-agent'];
42 | var crawler = utils.getCrawlerFromUserAgentString(userAgent);
43 | if (crawler) {
44 | statTracker.trackEvent(`site-crawl.${crawler}`);
45 | }
46 |
47 | } catch (err) {
48 | debugLog(err);
49 | }
50 | };
51 |
52 | const validateConfig = (cfg) => {
53 | const config = (cfg || {});
54 |
55 | if (!config.ignoredEndsWithActions) {
56 | config.ignoredEndsWithActions = "\.js|\.css|\.svg|\.png";
57 | }
58 |
59 | config.ignoredEndsWithActionsRegex = new RegExp(config.ignoredEndsWithActions + "$", "i");
60 |
61 |
62 | if (!config.ignoredStartsWithActions) {
63 | config.ignoredStartsWithActions = "ignoreStartsWithThis|andIgnoreStartsWithThis";
64 | }
65 |
66 | config.ignoredStartsWithActionsRegex = new RegExp("^" + config.ignoredStartsWithActions, "i");
67 |
68 | return config;
69 | };
70 |
71 | const middlewareWrapper = (statTracker, cfg) => {
72 | const config = validateConfig(cfg);
73 |
74 | const middleware = (req, res, next) => {
75 | const startTimeNanos = process.hrtime.bigint();
76 |
77 | onHeaders(res, () => {
78 | onHeadersListener(config, req, res.statusCode, startTimeNanos, statTracker);
79 | });
80 |
81 | next();
82 | };
83 |
84 | middleware.middleware = middleware;
85 |
86 | return middleware;
87 | };
88 |
89 | module.exports = middlewareWrapper;
90 |
91 |
92 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "btc-rpc-explorer",
3 | "version": "3.3.0",
4 | "description": "Open-source, self-hosted Bitcoin explorer",
5 | "private": false,
6 | "bin": "bin/cli.js",
7 | "scripts": {
8 | "start": "node ./bin/www",
9 | "test": "node ./bin/test.js",
10 | "miners": "node ./bin/refresh-mining-pool-configs.js",
11 | "integrity": "node ./bin/frontend-resource-integrity.js",
12 | "css-light-debug": "sass --style expanded --source-map scss --precision 6 ./public/scss/light.scss ./public/style/light.css",
13 | "css-dark-debug": "sass --style expanded --source-map scss --precision 6 ./public/scss/dark.scss ./public/style/dark.css",
14 | "css-debug": "npm-run-all css-light-debug css-dark-debug",
15 | "css-light": "sass --style compressed --precision 6 ./public/scss/light.scss ./public/style/light.css",
16 | "css-dark": "sass --style compressed --precision 6 ./public/scss/dark.scss ./public/style/dark.css",
17 | "css": "npm-run-all css-light css-dark"
18 | },
19 | "keywords": [
20 | "bitcoin",
21 | "btc",
22 | "blockchain"
23 | ],
24 | "author": "Dan Janosik ",
25 | "license": "MIT",
26 | "repository": {
27 | "type": "git",
28 | "url": "git+https://github.com/janoside/btc-rpc-explorer.git"
29 | },
30 | "dependencies": {
31 | "@janoside/app-utils": "github:janoside/app-utils#148cd2fb1ff3c002f15ae99a2548c87cfb4b3514",
32 | "async": "^3.2.2",
33 | "axios": "^0.27.2",
34 | "basic-auth": "^2.0.1",
35 | "bech32": "2.0.0",
36 | "bitcoinjs-lib": "^5.2.0",
37 | "bluebird": "^3.7.2",
38 | "body-parser": "^1.19.0",
39 | "bootstrap": "^5.1.3",
40 | "btc-rpc-client": "github:btc21/btc-rpc-client",
41 | "chart.js": "^3.5.0",
42 | "compression": "^1.7.4",
43 | "cookie-parser": "^1.4.5",
44 | "crypto-js": "^4.0.0",
45 | "csurf": "^1.11.0",
46 | "debug": "^4.3.1",
47 | "decimal.js": "^10.2.1",
48 | "dotenv": "^10.0.0",
49 | "electrum-client": "github:janoside/electrum-client",
50 | "express": "^4.17.1",
51 | "express-async-handler": "^1.1.4",
52 | "express-session": "^1.17.2",
53 | "jstransformer-markdown-it": "^2.1.0",
54 | "lru-cache": "^6.0.0",
55 | "markdown-it": "^13.0.0",
56 | "md5": "^2.3.0",
57 | "meow": "^9.0.0",
58 | "moment": "^2.29.1",
59 | "moment-duration-format": "^2.3.2",
60 | "morgan": "^1.10.0",
61 | "on-headers": "^1.0.2",
62 | "pidusage": "^3.0.0",
63 | "pug": "^3.0.2",
64 | "qrcode": "^1.4.4",
65 | "redis": "^3.0.2",
66 | "semver": "^7.3.5",
67 | "serve-favicon": "^2.5.0",
68 | "simple-git": "^3.7.1",
69 | "zeromq": "^5.2.8"
70 | },
71 | "optionalDependencies": {
72 | "event-loop-stats": "^1.3.0"
73 | },
74 | "devDependencies": {
75 | "npm-run-all": "^4.1.5",
76 | "sass": "^1.44.0"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/bin/refresh-mining-pool-configs.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | "use strict";
4 |
5 | const os = require("os");
6 | const path = require("path");
7 | const dotenv = require("dotenv");
8 | const fs = require("fs");
9 | const axios = require("axios");
10 |
11 | const utils = require("../app/utils.js");
12 | const coins = require("../app/coins.js");
13 |
14 | async function refreshMiningPoolsForCoin(coinName) {
15 | console.log(`Refreshing mining pools for ${coinName}...`);
16 |
17 | if (coins[coinName].miningPoolsConfigUrls) {
18 | const miningPoolsConfigDir = path.join(__dirname, "..", "public", "txt", "mining-pools-configs", coinName);
19 |
20 | fs.readdir(miningPoolsConfigDir, (err, files) => {
21 | if (err) {
22 | throw new Error(`Unable to delete existing files from '${miningPoolsConfigDir}'`);
23 | }
24 |
25 | files.forEach(function(file) {
26 | // delete existing file
27 | fs.unlinkSync(path.join(miningPoolsConfigDir, file));
28 | });
29 | });
30 |
31 | const miningPoolsConfigUrls = coins[coinName].miningPoolsConfigUrls;
32 |
33 | const promises = [];
34 |
35 | console.log(`${miningPoolsConfigUrls.length} mining pool config(s) found for ${coinName}`);
36 |
37 | for (let i = 0; i < miningPoolsConfigUrls.length; i++) {
38 | promises.push(refreshMiningPoolConfig(coinName, i, miningPoolsConfigUrls[i]));
39 | }
40 |
41 | await Promise.all(promises);
42 |
43 | console.log(`Refreshed ${miningPoolsConfigUrls.length} mining pool config(s) for ${coinName}\n---------------------------------------------`);
44 |
45 | } else {
46 | console.log(`No mining pool URLs configured for ${coinName}`);
47 |
48 | throw new Error(`No mining pool URLs configured for ${coinName}`);
49 | }
50 | }
51 |
52 | async function refreshMiningPoolConfig(coinName, index, url) {
53 | try {
54 | const response = await axios.get(url, { transformResponse: res => res });
55 |
56 | const filename = path.join(__dirname, "..", "public", "txt", "mining-pools-configs", coinName, index + ".json");
57 |
58 | fs.writeFileSync(filename, response.data, (err) => {
59 | console.log(`Error writing file '${filename}': ${err}`);
60 | });
61 |
62 | console.log(`Wrote '${coinName}/${index}.json' with contents of url: ${url}`);
63 |
64 | } catch (err) {
65 | console.log(`Error downloading mining pool config for ${coinName}: url=${url}`);
66 |
67 | throw err;
68 | }
69 | }
70 |
71 | async function refreshAllMiningPoolConfigs() {
72 | const outerPromises = [];
73 |
74 | for (let i = 0; i < coins.coins.length; i++) {
75 | const coinName = coins.coins[i];
76 |
77 | await refreshMiningPoolsForCoin(coinName);
78 | }
79 | }
80 |
81 | refreshAllMiningPoolConfigs().then(() => {
82 | process.exit();
83 | });
84 |
--------------------------------------------------------------------------------
/views/admin/app-stats.pug:
--------------------------------------------------------------------------------
1 | extends ../layout
2 |
3 | include ./admin-mixins.pug
4 |
5 | block headContent
6 | title App Stats
7 |
8 | block content
9 | +adminNav
10 |
11 |
12 | +pageTitle("App Stats")
13 |
14 | +contentSection("Performance Stats")
15 | .table-responsive
16 | table.table.table-borderless.table-striped
17 | thead
18 | tr
19 | th Name
20 | th.text-end Min
21 | th.text-end Avg
22 | th.text-end Max
23 | th.text-end Sum
24 | th.text-end Count
25 | th.text-end First
26 | th.text-end Latest
27 |
28 | tbody
29 | each item, itemIndex in performanceStats
30 | tr
31 | td #{item[0]}
32 | td.text-end #{Math.round(item[1].min).toLocaleString()}
33 | td.text-end #{Math.round(item[1].avg).toLocaleString()}
34 | td.text-end #{Math.round(item[1].max).toLocaleString()}
35 | td.text-end #{Math.round(item[1].sum).toLocaleString()}
36 | td.text-end #{Math.round(item[1].count).toLocaleString()}
37 | td.text-end
38 | - var dt = moment.duration(new Date().getTime() - item[1].firstDate.getTime());
39 | span #{dt.format()}
40 | td.text-end
41 | - var dt = moment.duration(new Date().getTime() - item[1].lastDate.getTime());
42 | span #{dt.format()}
43 |
44 |
45 | +contentSection("Event Stats")
46 | .table-responsive
47 | table.table.table-borderless.table-striped
48 | thead
49 | tr
50 | th Name
51 | th.text-end Count
52 |
53 | tbody
54 | each item, itemIndex in eventStats
55 | tr
56 | td #{item[0]}
57 | td.text-end #{item[1].toLocaleString()}
58 |
59 |
60 | +contentSection("Value Stats")
61 | .table-responsive
62 | table.table.table-borderless.table-striped
63 | thead
64 | tr
65 | th Name
66 | th.text-end Min
67 | th.text-end Avg
68 | th.text-end Max
69 | th.text-end Sum
70 | th.text-end Count
71 | th.text-end First
72 | th.text-end Latest
73 |
74 | tbody
75 | each item, itemIndex in valueStats
76 | tr
77 | td #{item[0]}
78 | td.text-end #{Math.round(item[1].min).toLocaleString()}
79 | td.text-end #{Math.round(item[1].avg).toLocaleString()}
80 | td.text-end #{Math.round(item[1].max).toLocaleString()}
81 | td.text-end #{Math.round(item[1].sum).toLocaleString()}
82 | td.text-end #{Math.round(item[1].count).toLocaleString()}
83 | td.text-end
84 | - var dt = moment.duration(new Date().getTime() - item[1].firstDate.getTime());
85 | span #{dt.format()}
86 | td.text-end
87 | - var dt = moment.duration(new Date().getTime() - item[1].lastDate.getTime());
88 | span #{dt.format()}
89 |
--------------------------------------------------------------------------------
/app/statTracker.js:
--------------------------------------------------------------------------------
1 | const debug = require("debug");
2 | const debugLog = debug("statTracker");
3 |
4 |
5 | let performanceStats = {};
6 | const trackPerformance = (name, time) => {
7 | if (!performanceStats[name]) {
8 | performanceStats[name] = {
9 | min: time,
10 | max: time,
11 | sum: 0,
12 | count: 0,
13 | firstDate: new Date()
14 | };
15 | }
16 |
17 | if (time < performanceStats[name].min) {
18 | performanceStats[name].min = time;
19 | }
20 |
21 | if (time > performanceStats[name].max) {
22 | performanceStats[name].max = time;
23 | }
24 |
25 | performanceStats[name].count++;
26 | performanceStats[name].sum += time;
27 | performanceStats[name].avg = performanceStats[name].sum / performanceStats[name].count;
28 | performanceStats[name].lastDate = new Date();
29 | };
30 |
31 | let valueStats = {};
32 | const trackValue = (name, val) => {
33 | if (!valueStats[name]) {
34 | valueStats[name] = {
35 | min: val,
36 | max: val,
37 | sum: 0,
38 | count: 0,
39 | firstDate: new Date()
40 | };
41 | }
42 |
43 | if (val < valueStats[name].min) {
44 | valueStats[name].min = val;
45 | }
46 |
47 | if (val > valueStats[name].max) {
48 | valueStats[name].max = val;
49 | }
50 |
51 | valueStats[name].count++;
52 | valueStats[name].sum += val;
53 | valueStats[name].avg = valueStats[name].sum / valueStats[name].count;
54 | valueStats[name].lastDate = new Date();
55 | };
56 |
57 | let eventStats = {};
58 | const trackEvent = (name, count=1) => {
59 | if (!eventStats[name]) {
60 | eventStats[name] = 0;
61 | }
62 |
63 | eventStats[name] += count;
64 | };
65 |
66 | const processAndReset = (perfFunc, valueFunc, eventFunc) => {
67 | for (const [key, value] of Object.entries(performanceStats)) {
68 | perfFunc(key, value);
69 |
70 | //debugLog(key + ": " + JSON.stringify(value));
71 | }
72 |
73 | for (const [key, value] of Object.entries(valueStats)) {
74 | valueFunc(key, value);
75 |
76 | //debugLog(key + ": " + JSON.stringify(value));
77 | }
78 |
79 | for (const [key, value] of Object.entries(eventStats)) {
80 | eventFunc(key, {count:value});
81 |
82 | //debugLog(key + ": " + JSON.stringify(value));
83 | }
84 |
85 | performanceStats = {};
86 | valueStats = {};
87 | eventStats = {};
88 | };
89 |
90 | const currentStats = () => {
91 | return {
92 | performance: performanceStats,
93 | event: eventStats,
94 | value: valueStats
95 | };
96 | };
97 |
98 | module.exports = {
99 | trackPerformance: trackPerformance,
100 | trackValue: trackValue,
101 | trackEvent: trackEvent,
102 |
103 | currentStats: currentStats,
104 | processAndReset: processAndReset
105 | };
106 |
107 |
108 |
--------------------------------------------------------------------------------
/views/projected-blocks-old.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title Projected Blocks
5 |
6 | block content
7 | +pageTitle("Projected Blocks")
8 |
9 | if (projectedBlocks)
10 | +contentSection
11 |
12 | .table-responsive
13 | table.table.table-borderless.table-striped.mb-0
14 | thead
15 | tr
16 | //th
17 | th
18 |
19 | th.text-end Height
20 |
21 | th.text-end
22 | span.border-dotted(title="UTC timestamp of the block.", data-bs-toggle="tooltip") Time
23 |
24 | th.text-end
25 | span.border-dotted(title="The number of transactions included in each block.", data-bs-toggle="tooltip") N(tx)
26 |
27 | th.text-end
28 | span.border-dotted(title="The average fee (sat/vB) for all block transactions.", data-bs-toggle="tooltip") Avg Fee
29 |
30 | th.text-end Σ Fees
31 |
32 | th.text-end % Full
33 |
34 | tbody
35 |
36 | each block, blockIndex in projectedBlocks
37 |
38 | tr
39 | td
40 | small.text-muted #{(blockIndex + 1).toLocaleString()}
41 |
42 | td.text-end
43 | | +#{(blockIndex + 1)}
44 |
45 | td.text-end
46 | | ~#{new Decimal(10 * (blockIndex + 1)).toDP(0)}m
47 |
48 |
49 | //- var timeAgoTime = moment.utc(new Date()).diff(moment.utc(new Date(parseInt(block.time) * 1000)));
50 | //- var timeAgo = moment.duration(timeAgoTime);
51 |
52 | //- var timeDiff = null;
53 |
54 |
55 | td.text-end #{(block.txCount || 0).toLocaleString()}
56 |
57 | td.text-end
58 | | #{block.avgFeeRate}
59 |
60 |
61 | td.text-end
62 | if (block.totalFees)
63 | - var currencyValue = new Decimal(block.totalFees);
64 | - var currencyValueDecimals = 3;
65 |
66 | if (userSettings.displayCurrency == "btc")
67 | +valueDisplaySpecial(block.totalFees, 4)
68 |
69 | else
70 | +valueDisplay(block.totalFees)
71 |
72 |
73 |
74 |
75 | td.text-end
76 | - var full = new Decimal(block.weight).dividedBy(coinConfig.maxBlockWeight).times(100);
77 | - var full2 = full.toDP(0);
78 |
79 |
80 | if (full >= 99 || full2 == 99)
81 | span.text-success 99+
82 |
83 | else
84 | span.text-primary #{full2}
85 |
86 |
87 |
88 |
89 |
90 |
91 | if (true)
92 | pre
93 | code.json #{JSON.stringify(topTxs, null, 4)}
94 | if (false)
95 | each block, blockIndex in projectedBlocks
96 | pre
97 | code.json #{JSON.stringify(block)}
98 |
99 | hr
100 |
101 | else
102 | p No blocks found
103 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | name: "CodeQL"
7 |
8 | on:
9 | push:
10 | branches: [master]
11 | pull_request:
12 | # The branches below must be a subset of the branches above
13 | branches: [master]
14 | schedule:
15 | - cron: '0 21 * * 4'
16 |
17 | jobs:
18 | analyze:
19 | name: Analyze
20 | runs-on: ubuntu-latest
21 |
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | # Override automatic language detection by changing the below list
26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
27 | language: ['javascript']
28 | # Learn more...
29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
30 |
31 | steps:
32 | - name: Checkout repository
33 | uses: actions/checkout@v2
34 | with:
35 | # We must fetch at least the immediate parents so that if this is
36 | # a pull request then we can checkout the head.
37 | fetch-depth: 2
38 |
39 | # If this run was triggered by a pull request event, then checkout
40 | # the head of the pull request instead of the merge commit.
41 | - run: git checkout HEAD^2
42 | if: ${{ github.event_name == 'pull_request' }}
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v1
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v1
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v1
72 |
--------------------------------------------------------------------------------
/app/api/addressApi.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const config = require("./../config.js");
4 | const coins = require("../coins.js");
5 | const utils = require("../utils.js");
6 |
7 | const coinConfig = coins[config.coin];
8 |
9 | const electrumAddressApi = require("./electrumAddressApi.js");
10 | const blockchainAddressApi = require("./blockchainAddressApi.js");
11 | const blockchairAddressApi = require("./blockchairAddressApi.js");
12 | const blockcypherAddressApi = require("./blockcypherAddressApi.js");
13 |
14 | function getSupportedAddressApis() {
15 | return ["blockchain.com", "blockchair.com", "blockcypher.com", "electrum", "electrumx"];
16 | }
17 |
18 | function getCurrentAddressApiFeatureSupport() {
19 | if (config.addressApi == "blockchain.com") {
20 | return {
21 | pageNumbers: true,
22 | sortDesc: true,
23 | sortAsc: true
24 | };
25 |
26 | } else if (config.addressApi == "blockchair.com") {
27 | return {
28 | pageNumbers: true,
29 | sortDesc: true,
30 | sortAsc: false
31 | };
32 |
33 | } else if (config.addressApi == "blockcypher.com") {
34 | return {
35 | pageNumbers: true,
36 | sortDesc: true,
37 | sortAsc: false
38 | };
39 |
40 | } else if (config.addressApi == "electrum" || config.addressApi == "electrumx") {
41 | return {
42 | pageNumbers: true,
43 | sortDesc: true,
44 | sortAsc: true
45 | };
46 | }
47 | }
48 |
49 | function getAddressDetails(address, scriptPubkey, sort, limit, offset) {
50 | return new Promise(function(resolve, reject) {
51 | var promises = [];
52 |
53 | if (config.addressApi == "blockchain.com") {
54 | promises.push(blockchainAddressApi.getAddressDetails(address, scriptPubkey, sort, limit, offset));
55 |
56 | } else if (config.addressApi == "blockchair.com") {
57 | promises.push(blockchairAddressApi.getAddressDetails(address, scriptPubkey, sort, limit, offset));
58 |
59 | } else if (config.addressApi == "blockcypher.com") {
60 | promises.push(blockcypherAddressApi.getAddressDetails(address, scriptPubkey, sort, limit, offset));
61 |
62 | } else if (config.addressApi == "electrum" || config.addressApi == "electrumx") {
63 | promises.push(electrumAddressApi.getAddressDetails(address, scriptPubkey, sort, limit, offset));
64 |
65 | } else {
66 | promises.push(new Promise(function(resolve, reject) {
67 | resolve({addressDetails:null, errors:["No address API configured"]});
68 | }));
69 | }
70 |
71 | Promise.all(promises).then(function(results) {
72 | if (results && results.length > 0) {
73 | resolve(results[0]);
74 |
75 | } else {
76 | resolve(null);
77 | }
78 | }).catch(function(err) {
79 | reject(err);
80 | });
81 | });
82 | }
83 |
84 |
85 |
86 | module.exports = {
87 | getSupportedAddressApis: getSupportedAddressApis,
88 | getCurrentAddressApiFeatureSupport: getCurrentAddressApiFeatureSupport,
89 | getAddressDetails: getAddressDetails
90 | };
--------------------------------------------------------------------------------
/views/snippets/utxo-set.pug:
--------------------------------------------------------------------------------
1 | include ../includes/shared-mixins.pug
2 |
3 | .mt-3
4 | +pageTabs(["Details", "JSON"])
5 |
6 |
7 | div.tab-content
8 | +pageTab("Details", true)
9 | +contentSection
10 | +summaryRow(3)
11 | +summaryItem("Last Updated", "The time this UTXO Set snapshot was produced")
12 | +timestamp(utxoSetSummary.lastUpdated / 1000, true)
13 |
14 | +summaryItem("Block Height", "The top block height at the time this UTXO Set snapshot was produced")
15 | a(href=`./block-height/${utxoSetSummary.height}`) #{utxoSetSummary.height.toLocaleString()}
16 |
17 | +summaryItem("Block Hash", "The top block hash at the time this UTXO Set snapshot was produced")
18 | a(href=`./block/${utxoSetSummary.bestblock}`) #{utils.ellipsizeMiddle(utxoSetSummary.bestblock, 12)}
19 | +copyTextButton(utxoSetSummary.bestblock)
20 |
21 | hr.mt-3.mb-3
22 |
23 | +summaryRow(2 + (utxoSetSummary.transactions ? 1 : 0) + (utxoSetSummary.disk_size ? 1 : 0) + (utxoSetSummary.total_unspendable_amount ? 1 : 0))
24 | +summaryItem("UTXO Count", "The total number of UTXOs, or Unspent Transaction Outputs, across the entire blockchain")
25 | | #{utxoSetSummary.txouts.toLocaleString()}
26 |
27 | +summaryItem("Coins", "The sum of all spendable BTC units across the entire blockchain")
28 | span #{parseFloat(utxoSetSummary.total_amount).toLocaleString()}
29 | +copyTextButton(utxoSetSummary.total_amount)
30 |
31 | if (utxoSetSummary.transactions)
32 | +summaryItem("Total Transactions", "The total number of transactions that have unspent outputs")
33 | | #{utxoSetSummary.transactions.toLocaleString()}
34 |
35 | if (utxoSetSummary.total_unspendable_amount)
36 | +summaryItem("Unspendable Coins", "The total amount of coins permanently excluded from the UTXO Set (only available if coinstatsindex is used)")
37 | span #{parseFloat(utxoSetSummary.total_unspendable_amount).toLocaleString()}
38 |
39 | if (utxoSetSummary.disk_size)
40 | +summaryItem("Disk Size", "The estimated size of the UTXO Set on disk")
41 | - var diskSize = utils.formatLargeNumber(utxoSetSummary.disk_size);
42 |
43 | | #{new Decimal(diskSize[0]).toDP(3)} #{diskSize[1].abbreviation}B
44 |
45 | if (utxoSetSummary.usingCoinStatsIndex)
46 | span.small.text-muted Note: The above UTXO Set snapshot was produced using this node's coin-stats index; using this index affects which properties are available. Comparing the data above to a UTXO Set snapshot produced on a node without the coin-stats index enabled will look slightly different.
47 |
48 | else
49 | span.small.text-muted Note: This node does not have the coin-stats index enabled so this UTXO Set snapshot was built without it. Comparing the data above to a UTXO Set snapshot produced on a node WITH the coin-stats index enabled will look slightly different.
50 |
51 |
52 | +pageTab("JSON", false)
53 | +contentSection
54 | pre
55 | code.json #{JSON.stringify(utxoSetSummary, null, 4)}
--------------------------------------------------------------------------------
/app/sso.js:
--------------------------------------------------------------------------------
1 | //
2 | // IMPORTANT MESSAGE!!!
3 | //
4 | // Dear contributor, please take great care when modifying this code.
5 | // It was written defensively with attention to a lot of details in order to prevent security issues.
6 | // As a result of such care it avoided a problem that occurred in RTL which had similar but subtly broken logic
7 | // see https://github.com/Ride-The-Lightning/RTL/issues/610
8 | //
9 | // So before you change anything, please think twice about the consequences.
10 |
11 | "use strict";
12 |
13 | const crypto = require('crypto');
14 | const fs = require('fs');
15 | const utils = require("./utils.js");
16 |
17 | const authCookieName = "btcexp_auth";
18 |
19 | function generateToken() {
20 | // Normally we would use 16 => 128 bits of entropy which is sufficiennt
21 | // But since we're going to base64 it and there would be padding (==),
22 | // It's a wasted space, why not use padding for some additional entropy? :)
23 | // The replacing is to make it URL-safe
24 | return crypto.randomBytes(18).toString("base64").replace(/\+/g, '-').replace(/\//g, '_')
25 | }
26 |
27 | function updateToken(tokenFile) {
28 | // This implements atomic update of the token file to avoid corrupted tokens causing trouble
29 | // If first saves the token into a temporary file and then moves it over. The move is atomic.
30 | // The token could also be synced but since the next boot overwrites it anyway, disk corruption
31 | // is not an issue.
32 | var newToken = generateToken();
33 | var tmpFileName = tokenFile + ".tmp";
34 | // It is important that we use the generated token, and NOT read back what was written.
35 | // This avoids using predictable token if filesystem gets corrupted (e.g. in case of ENOSPC).
36 | fs.writeFileSync(tmpFileName, newToken);
37 | fs.renameSync(tmpFileName, tokenFile);
38 |
39 | return newToken;
40 | }
41 |
42 | module.exports = (tokenFile, loginRedirect) => {
43 | // Reinitializing the token at start is important due to same reason we don't read it back.
44 | // It also avoids races when another process binds the same port and reads the token in order to later use
45 | // it to attack this app.
46 | var token = updateToken(tokenFile);
47 | var cookies = new Set();
48 |
49 | return (req, res, next) => {
50 | if (req.cookies && cookies.has(req.cookies[authCookieName])) {
51 | req.authenticated = true;
52 |
53 | return next();
54 | }
55 |
56 |
57 | let matchingToken = false;
58 | if (req.query.token) {
59 | try {
60 | // We use timingSafeEqual to avoid timing attacks
61 | matchingToken = crypto.timingSafeEqual(Buffer.from(req.query.token, "utf8"), Buffer.from(token, "utf8"));
62 |
63 | } catch (e) {
64 | utils.logError("23rheuweesaa", e);
65 | }
66 | }
67 |
68 | if (matchingToken) {
69 | req.authenticated = true;
70 | token = updateToken(tokenFile);
71 | let cookie = generateToken();
72 | cookies.add(cookie);
73 | res.cookie(authCookieName, cookie);
74 |
75 | return next();
76 | }
77 |
78 | if (loginRedirect) {
79 | res.redirect(loginRedirect);
80 |
81 | } else {
82 | res.sendStatus(401);
83 | }
84 | };
85 | }
86 |
--------------------------------------------------------------------------------
/app/api/blockchainAddressApi.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const axios = require("axios");
4 | const utils = require("./../utils.js");
5 |
6 |
7 | function getAddressDetails(address, scriptPubkey, sort, limit, offset) {
8 | return new Promise(async (resolve, reject) => {
9 | if (address.startsWith("bc1")) {
10 | reject({userText:"blockchain.com API does not support bc1 (native Segwit) addresses"});
11 |
12 | return;
13 | }
14 |
15 | if (sort == "asc") {
16 | // need to query the total number of tx first, then build paging info from that value
17 | try {
18 | const response = await axios.get(
19 | `https://blockchain.info/rawaddr/${address}?limit=1`,
20 | { headers: { 'User-Agent': 'axios' }});
21 |
22 | var blockchainJson = response.data;
23 |
24 | var txCount = blockchainJson.n_tx;
25 | var pageCount = parseInt(txCount / limit);
26 | var lastPageSize = limit;
27 | if (pageCount * limit < txCount) {
28 | lastPageSize = txCount - pageCount * limit;
29 | }
30 |
31 | var dynamicOffset = txCount - limit - offset;
32 | if (dynamicOffset < 0) {
33 | limit += dynamicOffset;
34 | dynamicOffset += limit;
35 | }
36 |
37 | getAddressDetailsSortDesc(address, limit, dynamicOffset).then(function(result) {
38 | result.txids.reverse();
39 |
40 | resolve({addressDetails:result});
41 |
42 | }).catch(function(err) {
43 | utils.logError("2308hsghse", err);
44 |
45 | reject(err);
46 | });
47 |
48 | } catch (err) {
49 | utils.logError("we0f8hasd0fhas", err);
50 |
51 | reject(fullError);
52 | }
53 | } else {
54 | getAddressDetailsSortDesc(address, limit, offset).then(function(result) {
55 | resolve({addressDetails:result});
56 |
57 | }).catch(function(err) {
58 | utils.logError("3208hwssse", err);
59 |
60 | reject(err);
61 | });
62 | }
63 | });
64 | }
65 |
66 | function getAddressDetailsSortDesc(address, limit, offset) {
67 | return new Promise(async (resolve, reject) => {
68 | try {
69 | const apiResponse = await axios.get(
70 | `https://blockchain.info/rawaddr/${address}?limit=${limit}&offset=${offset}`,
71 | { headers: { 'User-Agent': 'axios' }});
72 |
73 | var blockchainJson = apiResponse.data;
74 |
75 | var response = {};
76 |
77 | response.txids = [];
78 | response.blockHeightsByTxid = {};
79 | blockchainJson.txs.forEach(function(tx) {
80 | response.txids.push(tx.hash);
81 | response.blockHeightsByTxid[tx.hash] = tx.block_height;
82 | });
83 |
84 | response.txCount = blockchainJson.n_tx;
85 | response.hash160 = blockchainJson.hash160;
86 | response.totalReceivedSat = blockchainJson.total_received;
87 | response.totalSentSat = blockchainJson.total_sent;
88 | response.balanceSat = blockchainJson.final_balance;
89 | response.source = "blockchain.com";
90 |
91 | resolve(response);
92 |
93 | } catch (err) {
94 | utils.logError("32907shsghs", err);
95 |
96 | reject(err);
97 | }
98 | });
99 | }
100 |
101 |
102 | module.exports = {
103 | getAddressDetails: getAddressDetails
104 | };
105 |
--------------------------------------------------------------------------------
/views/admin/os-stats.pug:
--------------------------------------------------------------------------------
1 | extends ../layout
2 |
3 | include ./admin-mixins.pug
4 |
5 | block headContent
6 | title OS Stats
7 |
8 | block content
9 | +adminNav
10 |
11 |
12 | +pageTitle("OS Stats")
13 |
14 | .clearfix
15 | .row
16 | each itemName in appStatNames
17 | .col-lg-6.float-start
18 | +contentSection(itemName)
19 | canvas(id=`canvas-${itemName}`)
20 |
21 |
22 | block endOfBody
23 |
24 | +graphPageScriptSetup(true)
25 |
26 |
27 | script.
28 | var appStatNames = !{JSON.stringify(appStatNames)};
29 | var appStats = !{JSON.stringify(appStats)};
30 |
31 | var graphsById = {};
32 |
33 | $(document).ready(function() {
34 | var blue = "#007bff";
35 |
36 | for (var n = 0; n < appStatNames.length; n++) {
37 | var propName = appStatNames[n];
38 |
39 | var data = [];
40 | for (var i = 0; i < appStats[propName].length; i++) {
41 | var item = appStats[propName][i];
42 |
43 | data.push({x:new Date(item.time), y:item.value});
44 | }
45 |
46 | console.log(propName + " - " + JSON.stringify(data));
47 |
48 | createGraph(`canvas-${propName}`, [propName], [data], [blue]);
49 | }
50 | });
51 |
52 | function createGraph(chartid, names, datas, colors) {
53 | var datasets = [];
54 | var yaxes = [];
55 |
56 | for (var i = 0; i < names.length; i++) {
57 | datasets.push({
58 | label: names[i],
59 | data: datas[i],
60 | borderWidth: 1,
61 | borderColor: colors[i],
62 | backgroundColor: 'rgba(0, 0, 0, 0)',
63 | pointRadius: 1,
64 | lineTension: 0
65 | });
66 |
67 | yaxes.push({
68 | scaleLabel: {
69 | display: false,
70 | //labelString: names[i]
71 | },
72 | grid: {
73 | color: gridLineColor
74 | },
75 | });
76 | }
77 |
78 | // update data in graph if we already created, otherwise create anew
79 | if (graphsById[chartid]) {
80 | graph = graphsById[chartid];
81 | graph.data = { datasets: datasets };
82 | graph.update();
83 |
84 | } else {
85 | var ctx = document.getElementById(chartid).getContext('2d');
86 | var graph = new Chart(ctx, {
87 | type: 'line',
88 | data: {
89 | datasets: datasets
90 | },
91 | options: {
92 | // disable all animations
93 | animation: { duration: 0 },
94 | hover: { animationDuration: 0 },
95 | responsiveAnimationDuration: 0,
96 |
97 | legend: {
98 | display: (datasets.length > 1)
99 | },
100 | scales: {
101 | x: {
102 | type: 'time',
103 | position: 'bottom',
104 | time: {
105 | unit: 'hour'
106 | },
107 | scaleLabel: {
108 | display: true,
109 | labelString: 'Time'
110 | },
111 | grid: {
112 | color: gridLineColor
113 | },
114 | },
115 | y: {
116 | type: 'linear',
117 | position: 'left',
118 | scaleLabel: {
119 | display: false,
120 | },
121 | grid: {
122 | color: gridLineColor
123 | },
124 | },
125 | }
126 | }
127 | });
128 |
129 | graphsById[chartid] = graph;
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/views/next-block.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block headContent
4 | title Next Block
5 |
6 | block content
7 | +pageTitle("Next Block")
8 |
9 | +dismissableInfoAlert("miningTemplatePageNoteDismissed", "About Next Block...")
10 | .mb-2 The Next Block tool displays the output from the getblocktemplate command which is often used by miners for building a candidate for the next block. The transactions shown here were selected from your node's mempool and generally represent those with the highest (effective) fees.
11 | div These details are just a best-guess based on the current mempool and will change with each refresh.
12 |
13 | +pageTabs(["Details", "JSON"])
14 |
15 | .tab-content
16 | +pageTab("Details", true)
17 | +contentSection("Summary")
18 | +summaryRow(3)
19 | +summaryItem("Transactions")
20 | | #{blockTemplate.transactions.length.toLocaleString()}
21 |
22 | +summaryItem("Fee Rates", null, "sat/vB")
23 | | #{new Decimal(minFeeRate).toDP(2)} - #{new Decimal(maxFeeRate).toDP(2)}
24 |
25 | +summaryItem("Total Fees")
26 | - var subsidy = coinConfig.blockRewardFunction(blockTemplate.height, global.activeBlockchain);
27 | - var totalFees = new Decimal(blockTemplate.coinbasevalue).dividedBy(coinConfig.baseCurrencyUnit.multiplier).minus(subsidy);
28 |
29 | +valueDisplay(totalFees)
30 |
31 | if (true)
32 | +contentSection(blockTemplate.transactions.length.toLocaleString() + " Transaction" + (blockTemplate.transactions.length == 1 ? "" : "s"))
33 | .table-responsive
34 | table.table.table-borderless.table-striped.mb-0
35 | thead
36 | tr
37 | th.text-card-highlight.text-uppercase.fw-light #
38 | th.text-card-highlight.text-uppercase.fw-light ID
39 | th.text-end.text-card-highlight.fw-light
40 | span.text-uppercase Fee Rate
41 | small.ms-1 (sat/vB)
42 |
43 | th.text-end.text-card-highlight.text-uppercase.fw-light Fee
44 | th.text-end.text-card-highlight.fw-light
45 | span.text-uppercase Weight
46 | small.ms-1 (wu)
47 | //th Depends
48 |
49 | tbody
50 | each tx, txIndex in blockTemplate.transactions
51 | tr
52 | td
53 | small.text-muted #{(txIndex).toLocaleString()}
54 |
55 | td
56 | a(href=`./tx/${tx.txid}`) #{utils.ellipsizeMiddle(tx.txid, 16)}
57 |
58 | td.text-end
59 | | #{new Decimal(tx.fee).dividedBy(tx.weight).times(4).toDP(2)}
60 |
61 | if (tx.avgFeeRate)
62 | span.text-muted.ms-2 (
63 | span.border-dotted(title="Effective fee rate, including ancestor transactions", data-bs-toggle="tooltip")
64 | span.text-muted #{new Decimal(tx.avgFeeRate).toDP(2)}
65 | | )
66 |
67 |
68 | td.text-end
69 | +valueDisplay(new Decimal(tx.fee).dividedBy(coinConfig.baseCurrencyUnit.multiplier))
70 |
71 | td.text-end #{tx.weight.toLocaleString()}
72 | //td #{tx.depends.map(x => `#${(x - 1)}`).join(", ")}
73 |
74 |
75 | +pageTab("JSON")
76 | - var x = blockTemplate;
77 | - delete x.transactions;
78 |
79 | +contentSection("Template Details")
80 | pre
81 | code.json #{JSON.stringify(x, null, 4)}
82 |
--------------------------------------------------------------------------------
/routes/snippetRouter.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const debug = require("debug");
4 | const debugLog = debug("btcexp:router");
5 |
6 | const express = require('express');
7 | const csurf = require('csurf');
8 | const router = express.Router();
9 | const util = require('util');
10 | const moment = require('moment');
11 | const qrcode = require('qrcode');
12 | const bitcoinjs = require('bitcoinjs-lib');
13 | const sha256 = require("crypto-js/sha256");
14 | const hexEnc = require("crypto-js/enc-hex");
15 | const Decimal = require("decimal.js");
16 | const asyncHandler = require("express-async-handler");
17 |
18 | const utils = require('./../app/utils.js');
19 | const coins = require("./../app/coins.js");
20 | const config = require("./../app/config.js");
21 | const coreApi = require("./../app/api/coreApi.js");
22 | const addressApi = require("./../app/api/addressApi.js");
23 | const btcQuotes = require("./../app/coins/btcQuotes.js");
24 |
25 | const forceCsrf = csurf({ ignoreMethods: [] });
26 |
27 |
28 |
29 |
30 |
31 | router.get("/formatCurrencyAmount/:amt", function(req, res, next) {
32 | res.locals.currencyValue = req.params.amt;
33 |
34 | res.render("includes/value-display");
35 |
36 | next();
37 | });
38 |
39 | router.get("/quote/random", function(req, res, next) {
40 | res.locals.quoteIndex = utils.randomInt(0, btcQuotes.items.length);
41 | res.locals.quote = btcQuotes.items[res.locals.quoteIndex];
42 |
43 | res.render("snippets/quote");
44 |
45 | next();
46 | });
47 |
48 | router.get("/next-block", asyncHandler(async (req, res, next) => {
49 | const promises = [];
50 |
51 | const result = {};
52 |
53 | promises.push(utils.timePromise("api/next-block/getblocktemplate", async () => {
54 | let nextBlockEstimate = await utils.timePromise("api/next-block/getNextBlockEstimate", async () => {
55 | return await coreApi.getNextBlockEstimate();
56 | });
57 |
58 |
59 | result.txCount = nextBlockEstimate.blockTemplate.transactions.length;
60 |
61 | result.totalWeight = nextBlockEstimate.weight;
62 |
63 | result.minFeeRate = nextBlockEstimate.minFeeRate;
64 | result.maxFeeRate = nextBlockEstimate.maxFeeRate;
65 | result.minFeeTxid = nextBlockEstimate.minFeeTxid;
66 | result.maxFeeTxid = nextBlockEstimate.maxFeeTxid;
67 |
68 | result.totalFees = nextBlockEstimate.totalFees.toNumber();
69 | }));
70 |
71 | await utils.awaitPromises(promises);
72 |
73 | res.locals.minFeeRate = result.minFeeRate;
74 | res.locals.maxFeeRate = result.maxFeeRate;
75 | res.locals.txCount = result.txCount;
76 | res.locals.totalWeight = result.totalWeight;
77 | res.locals.totalFees = result.totalFees;
78 |
79 | res.render("snippets/index-next-block");
80 | }));
81 |
82 | router.get("/utxo-set", asyncHandler(async (req, res, next) => {
83 | const promises = [];
84 |
85 | promises.push(utils.timePromise("api/utxo-set", async () => {
86 | if (global.utxoSetSummary) {
87 | res.locals.utxoSetSummary = global.utxoSetSummary;
88 |
89 | } else {
90 | res.locals.utxoSetSummary = await coreApi.getUtxoSetSummary(true, true);
91 | }
92 | }));
93 |
94 | await utils.awaitPromises(promises);
95 |
96 | res.render("snippets/utxo-set");
97 | }));
98 |
99 | router.get("/timezone-refresh-toast", asyncHandler(async (req, res, next) => {
100 | res.render("snippets/tz-update-toast");
101 | }));
102 |
103 |
104 | router.get("/timestamp", asyncHandler(async (req, res, next) => {
105 | res.locals.timestamp = req.query.timestamp;
106 | res.locals.includeAgo = req.query.includeAgo ? (req.query.includeAgo == "true") : true;
107 | res.locals.formatString = req.query.formatString;
108 |
109 | res.render("snippets/timestamp");
110 | }));
111 |
112 |
113 |
114 |
115 | module.exports = router;
116 |
--------------------------------------------------------------------------------
/bin/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var debug = require("debug");
4 | var debugLog = debug("btcexp:config");
5 |
6 | // to debug arg settings, enable the below line:
7 | //debug.enable("btcexp:*");
8 |
9 | const args = require('meow')(`
10 | Usage
11 | $ btc-rpc-explorer [options]
12 |
13 | Options
14 | -p, --port port to bind http server [default: 3002]
15 | -i, --host host to bind http server [default: 127.0.0.1]
16 | -a, --basic-auth-password <..> protect web interface with a password [default: no password]
17 | -C, --coin crypto-coin to enable [default: BTC]
18 |
19 | -b, --bitcoind-uri connection URI for bitcoind rpc (overrides the options below)
20 | -H, --bitcoind-host hostname for bitcoind rpc [default: 127.0.0.1]
21 | -P, --bitcoind-port port for bitcoind rpc [default: 8332]
22 | -c, --bitcoind-cookie path to bitcoind cookie file [default: ~/.bitcoin/.cookie]
23 | -u, --bitcoind-user username for bitcoind rpc [default: none]
24 | -w, --bitcoind-pass password for bitcoind rpc [default: none]
25 |
26 | --address-api