├── 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('