├── .ackrc ├── .dockerignore ├── .github └── workflows │ └── start-gitlab.yml ├── .gitignore ├── .gitlab-ci.yml ├── .travis.yml ├── API.md ├── HACKS.md ├── LICENSE ├── README.md ├── babel.config.json ├── build.sh ├── cli.sh.in ├── client ├── .babelrc ├── index.pug ├── npm-shrinkwrap.json ├── opensearch.pug ├── package.json ├── patches │ ├── @cycle+history+7.4.0.patch │ └── snabbdom-pragma+2.8.0.patch └── src │ ├── app.js │ ├── components │ └── loading.js │ ├── const.js │ ├── driver │ ├── blinding.js │ ├── instascan.js │ ├── route.js │ └── search.js │ ├── l10n.js │ ├── lib │ ├── deduce-blinded.js │ ├── fees.js │ ├── libwally.js │ └── privacy-analysis.js │ ├── run-browser.js │ ├── run-server.js │ ├── rxjs.js │ ├── util.js │ └── views │ ├── addr.js │ ├── asset-list.js │ ├── asset.js │ ├── block.js │ ├── blocks-all.js │ ├── blocks.js │ ├── error.js │ ├── footer.js │ ├── home.js │ ├── index.js │ ├── lander.js │ ├── layout.js │ ├── loading.js │ ├── mempool.js │ ├── nav-toggle.js │ ├── navbar-menu.js │ ├── navbar.js │ ├── pushtx.js │ ├── scan.js │ ├── search.js │ ├── sub-navbar.js │ ├── transactions-all.js │ ├── transactions.js │ ├── tx-privacy-analysis.js │ ├── tx-segwit-gains.js │ ├── tx-vin.js │ ├── tx-vout.js │ ├── tx.js │ └── util.js ├── contrib ├── 0001-add-support-to-save-fee-estimates-without-shutting-d.patch ├── Dockerfile ├── Dockerfile.base ├── asset_registry_pubkey.asc ├── asset_registry_testnet_pubkey.asc ├── bitcoin-mainnet-explorer.conf.in ├── bitcoin-mainnet-pruned-for-liquid.conf.in ├── bitcoin-regtest-explorer.conf.in ├── bitcoin-signet-explorer.conf.in ├── bitcoin-testnet-explorer.conf.in ├── bitcoind-wait-sync.sh ├── docker-compose.yml ├── electrum-announce.sh ├── liquid-mainnet-explorer.conf.in ├── liquid-mainnet-private-bridge-torrc ├── liquid-mainnet-private-bridge.conf.in ├── liquid-mainnet-public-bridge-torrc ├── liquid-mainnet-public-bridge.conf.in ├── liquid-regtest-explorer.conf.in ├── liquid-testnet-explorer.conf.in ├── nginx-liquid-assets.conf.in ├── nginx-sync.conf.in ├── nginx.conf.in ├── runit_boot.sh ├── runits │ ├── bitcoin_for_liquid-log-config.runit │ ├── bitcoin_for_liquid-log.runit │ ├── bitcoin_for_liquid.runit │ ├── electrs-log-config.runit │ ├── electrs-log.runit │ ├── electrs.runit │ ├── liquid-assets-poller-log-config.runit │ ├── liquid-assets-poller-log.runit │ ├── liquid-assets-poller.runit │ ├── nginx-log-config.runit │ ├── nginx-log.runit │ ├── nginx.runit │ ├── nodedaemon-log-config.runit │ ├── nodedaemon-log.runit │ ├── nodedaemon.runit │ ├── prerenderer-log-config.runit │ ├── prerenderer-log.runit │ ├── prerenderer.runit │ ├── socat.runit │ ├── tor-log-config.runit │ ├── tor-log.runit │ ├── tor.runit │ ├── websocket-log-config.runit │ ├── websocket-log.runit │ └── websocket.runit └── sync-mempool.sh ├── dev-server.js ├── flavors ├── bitcoin-mainnet │ └── config.env ├── bitcoin-regtest │ ├── config.env │ └── www │ │ └── img │ │ ├── block.png │ │ ├── icons │ │ ├── menu-logo.png │ │ ├── minus.png │ │ ├── plus.png │ │ └── search.png │ │ └── transaction.png ├── bitcoin-signet │ ├── config.env │ └── www │ │ └── img │ │ ├── block.png │ │ ├── icons │ │ ├── menu-logo.png │ │ ├── minus.png │ │ ├── plus.png │ │ └── search.png │ │ └── transaction.png ├── bitcoin-testnet │ ├── config.env │ ├── extras.css │ └── www │ │ └── img │ │ ├── block.png │ │ ├── favicon.png │ │ ├── icons │ │ ├── menu-logo.svg │ │ ├── minus.png │ │ ├── plus.png │ │ └── search.png │ │ └── transaction.png ├── blockstream │ ├── LICENSE.md │ ├── config.env │ ├── electrum-banner.txt │ ├── electrum-hosts-bitcoin-mainnet.json │ ├── electrum-hosts-bitcoin-testnet.json │ ├── electrum-hosts-liquid-mainnet.json │ ├── extras.css │ └── www │ │ ├── favicon.ico │ │ └── img │ │ ├── blockstream-full-logo-light.png │ │ ├── blockstream-full-logo.png │ │ ├── f1b_blue.png │ │ ├── favicon.png │ │ ├── icons │ │ ├── blockstream-logo-text.svg │ │ ├── blockstream-logo.png │ │ ├── dark-logo-icon.svg │ │ ├── explorer_dark_logo.svg │ │ ├── explorer_logo.svg │ │ └── light-logo-icon.svg │ │ ├── linkedin_blue.png │ │ ├── social-sharing.png │ │ └── x-white.svg ├── liquid-mainnet │ ├── config.env │ ├── extras.css │ └── www │ │ └── img │ │ ├── block.png │ │ ├── favicon.png │ │ ├── icons │ │ ├── menu-logo.svg │ │ ├── minus.png │ │ ├── plus.png │ │ └── search.png │ │ └── transaction.png ├── liquid-regtest │ ├── config.env │ └── www │ │ └── img │ │ ├── block.png │ │ ├── icons │ │ ├── menu-logo.svg │ │ ├── minus.png │ │ ├── plus.png │ │ └── search.png │ │ └── transaction.png ├── liquid-testnet │ ├── config.env │ ├── extras.css │ └── www │ │ └── img │ │ ├── block.png │ │ ├── favicon.png │ │ ├── icons │ │ ├── menu-logo.svg │ │ ├── minus.png │ │ ├── plus.png │ │ └── search.png │ │ └── transaction.png └── liquid │ └── extras.css ├── gitlab ├── build.yml └── test.yml ├── lang ├── README.md ├── bg.json ├── bg.po ├── bs.json ├── bs.po ├── de.json ├── de.po ├── en.json ├── en.po ├── es.json ├── es.po ├── fr.json ├── fr.po ├── he.json ├── he.po ├── hr.json ├── hr.po ├── index.js ├── it.json ├── it.po ├── jp.json ├── jp.po ├── ko.json ├── ko.po ├── me.json ├── me.po ├── nl.json ├── nl.po ├── pt-pt.json ├── pt-pt.po ├── ru.json ├── ru.po ├── sr.json ├── sr.po ├── strings.txt ├── sv.json ├── sv.po ├── util │ ├── build-all-json.sh │ ├── build-all-po.sh │ ├── extract-all.sh │ ├── extract.js │ ├── fetch-transifex.sh │ ├── json2po.js │ ├── npm-shrinkwrap.json │ ├── package.json │ ├── po2json.js │ └── update-strings.sh ├── zh-cn.json └── zh-cn.po ├── npm-shrinkwrap.json ├── package.json ├── prerender-server ├── build.sh ├── client ├── npm-shrinkwrap.json ├── package.json ├── src │ ├── cluster.js │ └── server.js └── start.sh ├── render-view.js ├── run.sh └── www ├── bootstrap.min.css ├── font ├── ChakraPetch-Bold.ttf ├── Inter-VariableFont_opsz,wght.ttf ├── SourceSansPro-Regular.ttf ├── SourceSansPro-SemiBold.ttf └── inconsolata ├── img ├── Loading.gif ├── block.png ├── ellipse-2.svg ├── ellipse.svg ├── explorer-laser-lines.svg ├── favicon.png ├── github_blue.png ├── hero-explorer-api.svg ├── icons │ ├── Bitcoin Testnet-menu-logo.png │ ├── Bitcoin Testnet-menu-logo.svg │ ├── Bitcoin-menu-logo.svg │ ├── BitcoinTestnet-menu-logo.svg │ ├── Liquid-menu-logo.svg │ ├── LiquidTestnet-menu-logo.svg │ ├── apple.png │ ├── apple_dark.png │ ├── arrow.png │ ├── arrow_down.png │ ├── arrow_left_blu.png │ ├── arrow_right_blu.png │ ├── cancel.png │ ├── code.png │ ├── copy.png │ ├── database.svg │ ├── encryption.svg │ ├── google-play.png │ ├── google-play_dark.png │ ├── green_logo.svg │ ├── green_logo_light.svg │ ├── integrate.svg │ ├── integration.svg │ ├── issuance.svg │ ├── jade_logo.svg │ ├── jade_logo_light.svg │ ├── lbtc.svg │ ├── left-arrow.png │ ├── linux.png │ ├── linux_dark.png │ ├── menu-logo.svg │ ├── minus.png │ ├── moon_dark.png │ ├── moon_light.png │ ├── old-minus.png │ ├── old-plus.png │ ├── peg_in.png │ ├── peg_out.png │ ├── plus.png │ ├── pricing1.svg │ ├── pricing2.svg │ ├── privacy.svg │ ├── qrcode.svg │ ├── redundancy.svg │ ├── rest-api.svg │ ├── search.png │ ├── security-tokens.svg │ ├── switch-icon.png │ └── warning.svg ├── logos │ ├── aqua-dark.svg │ ├── aqua.svg │ ├── bitcoin-dev-kit-dark.svg │ ├── bitcoin-dev-kit.svg │ ├── blockstream-green-dark.svg │ ├── blockstream-green.svg │ ├── bull-bitcoin.svg │ ├── lwk-dark.svg │ ├── lwk.svg │ ├── nunchuk-dark.svg │ ├── nunchuk.svg │ ├── sideswap-dark.svg │ ├── sideswap.svg │ ├── sparrow-dark.png │ └── sparrow.png ├── onion.png ├── onion_light.png ├── rest-api-cta.svg └── transaction.png ├── instascan.min.js ├── js └── infinite-scroll.js ├── light-theme_style.css ├── robots.txt └── style.css /.ackrc: -------------------------------------------------------------------------------- 1 | --ignore-dir=dist 2 | --ignore-dir=www/img 3 | --ignore-dir=www/font 4 | --ignore-file=is:bootstrap.min.css 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | README.md 4 | Dockerfile 5 | .gitlab-ci.yml 6 | terraform 7 | node_modules 8 | client/node_modules 9 | prerender-server/node_modules 10 | *_datadir/ 11 | data_bitcoin_mainnet 12 | data_liquid_mainnet 13 | data_bitcoin_testnet 14 | data_bitcoin_regtest 15 | data_liquid_regtest 16 | www/libwally 17 | data_liquid_testnet 18 | data_bitcoin_signet 19 | gitlab/ -------------------------------------------------------------------------------- /.github/workflows/start-gitlab.yml: -------------------------------------------------------------------------------- 1 | name: Start GitLab CI 2 | on: 3 | # Use pull_request_target to run the workflow from the base branch (e.g., main) 4 | # This ensures the trusted workflow logic executes, even for PRs from forks. 5 | # It also grants access to secrets needed for the trigger. 6 | pull_request_target: 7 | types: [opened, synchronize, reopened] 8 | jobs: 9 | trigger-gitlab: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Start Gitlab Pipeline 13 | env: 14 | # Get trigger config from secrets 15 | GL_TRIGGER_TOKEN: ${{ secrets.GL_TRIGGER_TOKEN }} 16 | GL_TRIGGER_URL: ${{ secrets.GL_TRIGGER_URL }} 17 | # Use a specific ref from secrets if provided, otherwise default to the PR's head branch name 18 | GL_TRIGGER_REF: ${{ secrets.GL_TRIGGER_REF || github.event.pull_request.head.ref }} 19 | # --- Variables to pass to GitLab --- 20 | # The commit SHA in the GitHub PR 21 | GITHUB_PR_SHA: ${{ github.event.pull_request.head.sha }} 22 | # The ref (branch name) of the PR head 23 | GITHUB_PR_REF: ${{ github.event.pull_request.head.ref }} 24 | # The repository name (e.g., 'your-org/your-repo') 25 | GITHUB_REPO: ${{ github.repository }} 26 | # The GitHub token for reporting status back 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | run: | 29 | # --- Safety Checks --- 30 | # Ensure critical secrets are actually available (they should be with pull_request_target) 31 | if [ -z "$GL_TRIGGER_TOKEN" ]; then 32 | echo "::error::GL_TRIGGER_TOKEN secret is missing or unavailable!" 33 | exit 1 34 | fi 35 | if [ -z "$GITHUB_TOKEN" ]; then 36 | echo "::error::GITHUB_TOKEN is empty. Secrets may not be properly accessed." 37 | exit 1 38 | fi 39 | # Ensure URL is set 40 | if [ -z "$GL_TRIGGER_URL" ]; then 41 | echo "::error::GL_TRIGGER_URL secret is missing or unavailable!" 42 | exit 1 43 | fi 44 | 45 | echo "Triggering GitLab pipeline for SHA: ${GITHUB_PR_SHA}" 46 | curl --fail --silent --show-error --request POST \ 47 | --form token="${GL_TRIGGER_TOKEN}" \ 48 | --form ref="${GL_TRIGGER_REF}" \ 49 | --form "variables[GITHUB_PR_SHA]=${GITHUB_PR_SHA}" \ 50 | --form "variables[GITHUB_PR_REF]=${GITHUB_PR_REF}" \ 51 | --form "variables[GITHUB_REPO]=${GITHUB_REPO}" \ 52 | "${GL_TRIGGER_URL}" > /dev/null 53 | echo "GitLab pipeline triggered." 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | terraform/.terraform 4 | terraform/.terraform/environment 5 | terraform/*.tfstate.backup 6 | terraform/*.tfstate 7 | *~ 8 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | DOCKER_HOST: tcp://docker:2375 3 | DOCKER_TLS_CERTDIR: "" 4 | DOCKER_BUILDKIT: 1 5 | CI_DISPOSABLE_ENVIRONMENT: "true" 6 | IMAGE_BASE: blockstream/esplora-base 7 | IMAGE: blockstream/esplora 8 | DOCKERHUB_ESPLORA_URL: "https://hub.docker.com/v2/repositories/blockstream/esplora/tags/" 9 | 10 | default: 11 | image: docker:27 12 | services: 13 | - name: docker:27-dind 14 | command: ["dockerd", "--host=tcp://0.0.0.0:2375", "--mtu=1450"] 15 | alias: "docker" 16 | tags: 17 | - cloud 18 | retry: 19 | max: 2 20 | when: 21 | - runner_system_failure 22 | - unknown_failure 23 | - stuck_or_timeout_failure 24 | 25 | stages: 26 | - build 27 | 28 | include: 29 | - "gitlab/**.yml" 30 | 31 | ## disables MR-triggered pipelines and allows only branch-triggered pipelines 32 | workflow: 33 | rules: 34 | - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' 35 | when: never 36 | - when: always 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | 5 | script: 6 | - docker build -t blockstream/esplora-base -f Dockerfile.deps . 7 | - docker build . 8 | -------------------------------------------------------------------------------- /HACKS.md: -------------------------------------------------------------------------------- 1 | - @cycle/history had to be patched (via patch-package) to properly handle `` 2 | 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2021 Blockstream Corp 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "@babel/env" ] 3 | , "babelrcRoots": [ ".", "client" ] 4 | } 5 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeo pipefail 3 | shopt -s extglob 4 | 5 | for flavor in "$@"; do source flavors/$flavor/config.env; done 6 | 7 | export DEST=${DEST:-dist} 8 | export NODE_ENV=${NODE_ENV:=production} 9 | export BASE_HREF=${BASE_HREF:-/} 10 | export API_URL=${API_URL:-"${BASE_HREF}api"} 11 | 12 | mkdir -p $DEST 13 | rm -rf $DEST/* 14 | 15 | [[ -d node_modules ]] || npm install 16 | (cd client && [[ -d node_modules ]] || npm install) 17 | 18 | # Static assets 19 | cp -RL www/* $CUSTOM_ASSETS $DEST/ 20 | 21 | # CSS customizations 22 | [ -n "$CUSTOM_CSS" ] && cat $CUSTOM_CSS >> $DEST/style.css 23 | 24 | # Index HTML 25 | pug client/index.pug -o $DEST 26 | 27 | # Open search (requires absolute CANONICAL_URL) 28 | if [ -n "$CANONICAL_URL" ]; then 29 | pug client/opensearch.pug -E xml -o $DEST 30 | fi 31 | 32 | # RTLify CSS 33 | cat $DEST/style.css | node -p "require('cssjanus').transform(fs.readFileSync('/dev/stdin').toString(), false, true)" > $DEST/style-rtl.css 34 | 35 | # Browserify bundle 36 | # --no-dedupe needed due to https://github.com/substack/bundle-collapser/issues/20 https://github.com/browserify/browserify/issues/1450 37 | (cd client && browserify --no-dedupe -p bundle-collapser/plugin src/run-browser.js \ 38 | | ( [[ "$NODE_ENV" != "development" ]] && uglifyjs -cm || cat ) ) \ 39 | > $DEST/app.js 40 | 41 | # Pre-render notfound.html 42 | babel-node render-view.js '{"view":"error","error":"Page Not Found"}' > $DEST/notfound.html 43 | -------------------------------------------------------------------------------- /cli.sh.in: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | /srv/explorer/{DAEMON}/bin/{DAEMON}-cli -conf=/data/.{DAEMON}.conf -datadir=/data/{DAEMON} "$@" 5 | -------------------------------------------------------------------------------- /client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: [ "@babel/env" ] 3 | , plugins: [ 4 | ["@babel/plugin-transform-react-jsx", {"pragma": "Snabbdom.createElement"}] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /client/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | - baseTitle = process.env.SITE_TITLE || 'Block Explorer' 4 | - staticRoot = process.env.STATIC_ROOT || '' 5 | - pageTitle = prerender_title ? `${prerender_title} · ${baseTitle}` : baseTitle 6 | - bodyClass = theme ? `theme-${theme}` : '' 7 | 8 | - cssPath = t ? t`style.css` : 'style.css' 9 | - lang = t ? t.lang_id : 'en' 10 | - dir = t ? t`ltr` : 'ltr' 11 | 12 | head 13 | meta(charset='utf-8') 14 | title= pageTitle 15 | meta(property='og:title', content=process.env.OG_TITLE || process.env.SITE_TITLE || 'Block explorer') 16 | meta(name='description', content=process.env.SITE_DESC || 'Esplora Block Explorer') 17 | 18 | if canon_url 19 | link(rel="canonical", href=canon_url) 20 | 21 | if !noscript && process.env.NOSCRIPT_REDIR 22 | noscript: meta(http-equiv='refresh', content="0; url='?nojs'") 23 | 24 | base(href=process.env.BASE_HREF || '/') 25 | 26 | meta(name='viewport', content='width=device-width, initial-scale=1') 27 | link(rel='shortcut icon', type='image/png', href=staticRoot+'img/favicon.png') 28 | link(rel='stylesheet', href=staticRoot+'bootstrap.min.css') 29 | link(rel='stylesheet', href=staticRoot+cssPath) 30 | 31 | if process.env.CANONICAL_URL 32 | //- open search requires the absolute URL of the explorer 33 | link(rel='search', href=staticRoot+'opensearch.xml', type='application/opensearchdescription+xml', title=process.env.SITE_TITLE || 'Block Explorer') 34 | 35 | != process.env.HEAD_HTML 36 | 37 | body(class=bodyClass, lang=lang, dir=dir) 38 | #explorer!= prerender_html || '' 39 | 40 | if !noscript 41 | script(src=staticRoot+'app.js', async) 42 | != process.env.FOOT_HTML 43 | -------------------------------------------------------------------------------- /client/opensearch.pug: -------------------------------------------------------------------------------- 1 | doctype xml 2 | 3 | - canonUrl = process.env.CANONICAL_URL.replace(/\/$/, '') 4 | 5 | OpenSearchDescription(xmlns='http://a9.com/-/spec/opensearch/1.1/', xmlns:moz='http://www.mozilla.org/2006/browser/search/') 6 | ShortName= process.env.SITE_TITLE || 'Esplora Block Explorer' 7 | Description= process.env.SITE_DESC || 'Esplora Block Explorer' 8 | InputEncoding UTF-8 9 | Image(width='16', height='16', type='image/x-icon')= `${canonUrl}/img/favicon.png` 10 | Url(type='text/html', method='GET', template=`${canonUrl}/{searchTerms}`) 11 | SearchForm= canonUrl 12 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esplora-client", 3 | "version": "0.1.0", 4 | "author": "Nadav Ivgi", 5 | "license": "MIT", 6 | "scripts": { 7 | "postinstall": "../node_modules/.bin/patch-package", 8 | "dist": "../node_modules/.bin/babel -d dist src" 9 | }, 10 | "dependencies": { 11 | "@babel/plugin-transform-react-jsx": "^7.12.11", 12 | "@babel/polyfill": "^7.12.1", 13 | "@cycle/dom": "^22.8.0", 14 | "@cycle/history": "^7.4.0", 15 | "@cycle/html": "^3.4.0", 16 | "@cycle/http": "^15.4.0", 17 | "@cycle/rxjs-run": "^10.5.0", 18 | "@cycle/storage": "^5.1.2", 19 | "babelify": "^10.0.0", 20 | "basic-l10n": "^2.0.0", 21 | "bootstrap": "^4.4.1", 22 | "browserify-package-json": "^1.0.1", 23 | "bs58check": "^2.1.2", 24 | "bundle-collapser": "^1.4.0", 25 | "debug": "^4.3.1", 26 | "envify": "^4.1.0", 27 | "fmtbtc": "0.0.3", 28 | "in-browser-language": "^1.0.3", 29 | "instascan": "github:shesek/instascan#packaged-lib", 30 | "jquery": "^3.5.1", 31 | "path-to-regexp": "^6.2.0", 32 | "qrcode": "^1.4.4", 33 | "rxjs": "^6.6.3", 34 | "rxjs-compat": "^6.6.3", 35 | "snabbdom-pragma": "^2.8.0", 36 | "superagent": "^6.1.0", 37 | "uglifyify": "^5.0.2" 38 | }, 39 | "browserify": { 40 | "transform": [ 41 | "babelify", 42 | [ 43 | "envify", 44 | { 45 | "_": "purge" 46 | } 47 | ], 48 | "uglifyify", 49 | [ 50 | "browserify-package-json", 51 | { 52 | "only": "version" 53 | } 54 | ] 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /client/patches/@cycle+history+7.4.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@cycle/history/lib/cjs/captureClicks.js b/node_modules/@cycle/history/lib/cjs/captureClicks.js 2 | index 36e6b3f..616874f 100644 3 | --- a/node_modules/@cycle/history/lib/cjs/captureClicks.js 4 | +++ b/node_modules/@cycle/history/lib/cjs/captureClicks.js 5 | @@ -17,6 +17,9 @@ function sameOrigin(href) { 6 | } 7 | return href && href.indexOf(window.location.origin) === 0; 8 | } 9 | + 10 | +var baseHref = document.querySelector('base').getAttribute('href'); 11 | + 12 | function makeClickListener(push) { 13 | return function clickListener(event) { 14 | if (which(event) !== 1) { 15 | @@ -51,7 +54,13 @@ function makeClickListener(push) { 16 | } 17 | event.preventDefault(); 18 | var pathname = element.pathname, search = element.search, _a = element.hash, hash = _a === void 0 ? '' : _a; 19 | - push(pathname + search + hash); 20 | + 21 | + // strip base href from the pathname. it gets added back by the history driver, which results 22 | + // in having it twice without this fix. 23 | + if (pathname.indexOf(baseHref) == 0) pathname = pathname.substr(baseHref.length-1); 24 | + 25 | + // temporary fix for https://github.com/cyclejs/cyclejs/issues/909, until a solution is merged into cycle 26 | + push({ pathname, search, hash }); 27 | }; 28 | } 29 | function captureAnchorClicks(push) { 30 | @@ -70,8 +79,8 @@ function captureClicks(historyDriver) { 31 | start: function () { }, 32 | stop: function () { return typeof cleanup === 'function' && cleanup(); }, 33 | }); 34 | - cleanup = captureAnchorClicks(function (pathname) { 35 | - internalSink$._n({ type: 'push', pathname: pathname }); 36 | + cleanup = captureAnchorClicks(function (loc) { 37 | + internalSink$._n(Object.assign({ type: 'push' }, loc)); 38 | }); 39 | sink$._add(internalSink$); 40 | return historyDriver(internalSink$); 41 | diff --git a/node_modules/@cycle/history/lib/cjs/createHistory$.js b/node_modules/@cycle/history/lib/cjs/createHistory$.js 42 | index 350fd1a..e8cdabb 100644 43 | --- a/node_modules/@cycle/history/lib/cjs/createHistory$.js 44 | +++ b/node_modules/@cycle/history/lib/cjs/createHistory$.js 45 | @@ -32,7 +32,7 @@ function makeCallOnHistory(history) { 46 | history.push(__assign({}, input)); 47 | } 48 | if (input.type === 'replace') { 49 | - history.replace(input.pathname, input.state); 50 | + history.replace(__assign({}, input)); 51 | } 52 | if (input.type === 'go') { 53 | history.go(input.amount); 54 | -------------------------------------------------------------------------------- /client/patches/snabbdom-pragma+2.8.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/snabbdom-pragma/dist/index.es6.js b/node_modules/snabbdom-pragma/dist/index.es6.js 2 | index 77f626b..6231d3e 100644 3 | --- a/node_modules/snabbdom-pragma/dist/index.es6.js 4 | +++ b/node_modules/snabbdom-pragma/dist/index.es6.js 5 | @@ -169,7 +169,7 @@ var createElementWithModules = function (modules) { 6 | return considerSvg({ 7 | sel: sel, 8 | data: data ? sanitizeData(data, modules) : {}, 9 | - children: text$$1 ? undefined : sanitizeChildren(children), 10 | + children: !undefinedv(text$$1) ? undefined : sanitizeChildren(children), 11 | text: text$$1, 12 | elm: undefined, 13 | key: data ? data.key : undefined 14 | diff --git a/node_modules/snabbdom-pragma/dist/index.js b/node_modules/snabbdom-pragma/dist/index.js 15 | index db2b2ed..18e4d58 100644 16 | --- a/node_modules/snabbdom-pragma/dist/index.js 17 | +++ b/node_modules/snabbdom-pragma/dist/index.js 18 | @@ -175,7 +175,7 @@ var createElementWithModules = function (modules) { 19 | return considerSvg({ 20 | sel: sel, 21 | data: data ? sanitizeData(data, modules) : {}, 22 | - children: text$$1 ? undefined : sanitizeChildren(children), 23 | + children: !undefinedv(text$$1) ? undefined : sanitizeChildren(children), 24 | text: text$$1, 25 | elm: undefined, 26 | key: data ? data.key : undefined 27 | diff --git a/node_modules/snabbdom-pragma/src/index.js b/node_modules/snabbdom-pragma/src/index.js 28 | index 3c46dff..56c25c2 100644 29 | --- a/node_modules/snabbdom-pragma/src/index.js 30 | +++ b/node_modules/snabbdom-pragma/src/index.js 31 | @@ -87,7 +87,7 @@ export const createElementWithModules = (modules) => { 32 | return considerSvg({ 33 | sel, 34 | data: data ? sanitizeData(data, modules) : {}, 35 | - children: text ? undefined : sanitizeChildren(children), 36 | + children: !is.undefinedv(text) ? undefined : sanitizeChildren(children), 37 | text, 38 | elm: undefined, 39 | key: data ? data.key : undefined 40 | -------------------------------------------------------------------------------- /client/src/components/loading.js: -------------------------------------------------------------------------------- 1 | import Snabbdom from 'snabbdom-pragma' 2 | 3 | export default (size) => 4 |
5 |
6 |
-------------------------------------------------------------------------------- /client/src/const.js: -------------------------------------------------------------------------------- 1 | export const blockTxsPerPage = 25 2 | export const addrTxsPerPage = 25 3 | export const blocksPerPage = 10 4 | export const maxMempoolTxs = 50 5 | 6 | export const nativeAssetId = process.env.NATIVE_ASSET_ID || '6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d' 7 | export const nativeAssetLabel = process.env.NATIVE_ASSET_LABEL || 'BTC' 8 | export const nativeAssetName = process.env.NATIVE_ASSET_NAME || 'Bitcoin' 9 | 10 | // Elements only 11 | export const assetTxsPerPage = 25 12 | export const pegTxsPerPage = 25 13 | -------------------------------------------------------------------------------- /client/src/driver/blinding.js: -------------------------------------------------------------------------------- 1 | import { Observable as O } from '../rxjs' 2 | import * as libwally from '../lib/libwally' 3 | 4 | // Accepts a stream of blinding data strings, returns a stream of Unblinded 5 | // objects with a map from the commitments to the unblinded data 6 | module.exports = blinders_str$ => 7 | O.from(blinders_str$).flatMap(async blinders_str => { 8 | if (!blinders_str) return null 9 | 10 | await libwally.load() 11 | 12 | try { 13 | const blinders = parseBlinders(blinders_str) 14 | return new Unblinded(blinders) 15 | } 16 | catch (error) { 17 | return { error } 18 | } 19 | }) 20 | .share() 21 | 22 | class Unblinded { 23 | constructor(blinders) { 24 | this.commitments = makeCommitmentMap(blinders) 25 | } 26 | 27 | // Look for the given output, returning an { value, asset } object 28 | find(vout) { 29 | return vout.assetcommitment && vout.valuecommitment && 30 | this.commitments.get(`${vout.assetcommitment}:${vout.valuecommitment}`) 31 | } 32 | 33 | // Lookup all transaction inputs/outputs and attach the unblinded data 34 | tryUnblindTx(tx) { 35 | if (tx._unblinded) return tx._unblinded 36 | let matched = 0 37 | tx.vout.forEach(vout => matched += +this.tryUnblindOut(vout)) 38 | tx.vin.filter(vin => vin.prevout).forEach(vin => matched += +this.tryUnblindOut(vin.prevout)) 39 | tx._unblinded = { matched, total: this.commitments.size } 40 | tx._deduced = false // invalidate cache so deduction is attempted again 41 | return tx._unblinded 42 | } 43 | 44 | // Look the given output and attach the unblinded data 45 | tryUnblindOut(vout) { 46 | const unblinded = this.find(vout) 47 | if (unblinded) Object.assign(vout, unblinded) 48 | return !!unblinded 49 | } 50 | } 51 | 52 | function makeCommitmentMap(blinders) { 53 | const commitments = new Map 54 | 55 | blinders.forEach(b => { 56 | const { asset_commitment, value_commitment } = 57 | libwally.generate_commitments(b.value, b.asset, b.value_blinder, b.asset_blinder) 58 | 59 | commitments.set(`${asset_commitment}:${value_commitment}`, { 60 | asset: b.asset, 61 | value: b.value, 62 | }) 63 | }) 64 | 65 | return commitments 66 | } 67 | 68 | // Parse the blinders data from a string encoded as a comma separated list, in the following format: 69 | // ,,, 70 | // This can be repeated with a comma separator to specify blinders for multiple outputs. 71 | 72 | function parseBlinders(str) { 73 | const parts = str.split(',') 74 | , blinders = [] 75 | 76 | while (parts.length) { 77 | blinders.push({ 78 | value: verifyNum(parts.shift()) 79 | , asset: verifyHex32(parts.shift()) 80 | , value_blinder: verifyHex32(parts.shift()) 81 | , asset_blinder: verifyHex32(parts.shift()) 82 | }) 83 | } 84 | return blinders 85 | } 86 | 87 | function verifyNum(num) { 88 | if (!+num) throw new Error('Invalid blinding data (invalid number)') 89 | return +num 90 | } 91 | function verifyHex32(str) { 92 | if (!str || !/^[0-9a-f]{64}$/i.test(str)) throw new Error('Invalid blinding data (invalid hex)') 93 | return str 94 | } 95 | -------------------------------------------------------------------------------- /client/src/driver/instascan.js: -------------------------------------------------------------------------------- 1 | import { Observable as O } from '../rxjs' 2 | 3 | const staticRoot = process.env.STATIC_ROOT || '' 4 | 5 | // check for WebRTC camera support 6 | if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { 7 | 8 | // load instascan.js on demand when used for the first time 9 | let loaded = false 10 | function load() { 11 | if (loaded) return; 12 | loaded = true; 13 | const script = document.createElement('script') 14 | script.src = `${staticRoot}instascan.min.js` 15 | document.body.appendChild(script) 16 | } 17 | 18 | const Instascan$ = O.fromEvent(document.body, 'load', true).filter(e => e.target.src.endsWith('/instascan.min.js')).map(_ => window.Instascan).share() 19 | , Scanner$ = Instascan$.map(Instascan => Instascan.Scanner) 20 | , Camera$ = Instascan$.map(Instascan => Instascan.Camera) 21 | 22 | const makeScanDriver = (opt={}) => { 23 | const video = document.createElement('video') 24 | , scanner$ = Scanner$.map(Scanner => new Scanner({ ...opt, video })).shareReplay(1) 25 | , active$ = scanner$.flatMap(scanner => O.fromEvent(scanner, 'active')).share() 26 | , scan$ = scanner$.flatMap(scanner => O.fromEvent(scanner, 'scan')) 27 | .map(s => Array.isArray(s) ? s.filter(Boolean).join('') : s).share() 28 | // for some QRs, the QR content is provided as an array of segments, which may include nulls. 29 | 30 | video.className = 'qr-video' 31 | document.body.appendChild(video) 32 | 33 | function startScan(Camera, scanner) { 34 | load() 35 | Camera.getCameras().then(pickCam).then(cam => { 36 | document.body.classList.add('qr-scanning') 37 | scanner.start(cam) 38 | }) 39 | } 40 | 41 | function stopScan(scanner) { 42 | document.body.classList.remove('qr-scanning') 43 | scanner.stop() 44 | } 45 | 46 | return _mode$ => { 47 | const mode$ = O.from(_mode$) 48 | 49 | mode$.filter(Boolean).subscribe(load) 50 | 51 | // start/stop scanner according to mode$ 52 | O.combineLatest(mode$, Camera$, scanner$).subscribe(([ mode, Camera, scanner ]) => 53 | mode ? startScan(Camera, scanner) : stopScan(scanner)) 54 | 55 | // if the scanner becomes active while mode$ is off, turn it off again 56 | // without this, starting the scanner then quickly stopping it before it fully initialized could get it stuck on screen 57 | active$.withLatestFrom(mode$, scanner$) 58 | .subscribe(([ active, mode, scanner ]) => (!mode && setTimeout(_ => scanner.stop(), 100))) 59 | 60 | return scan$ 61 | } 62 | } 63 | 64 | const pickCam = cams => 65 | cams.find(cam => cam.name && cam.name.includes('back')) 66 | || cams[0] 67 | 68 | module.exports = makeScanDriver 69 | } 70 | 71 | else { 72 | // if we don't have WebTC camera support, return a noop driver 73 | module.exports = _ => _ => O.empty() 74 | } 75 | -------------------------------------------------------------------------------- /client/src/driver/route.js: -------------------------------------------------------------------------------- 1 | import qs from 'querystring' 2 | import { pathToRegexp } from 'path-to-regexp' 3 | import { Observable as O } from '../rxjs' 4 | 5 | const isStr = x => typeof x === 'string' 6 | 7 | const makeObj = (keys, values) => keys.reduce((o, k, i) => ({ ...o, [k.name]: values[i] }), {}) 8 | 9 | const baseHref = process.env.BASE_HREF || '/' 10 | , stripBase = path => path.indexOf(baseHref) == 0 ? path.substr(baseHref.length-1) : path 11 | 12 | const parseQuery = loc => { 13 | const query = loc.search ? qs.parse(loc.search.substr(1)) : {} 14 | 15 | // Convert value-less args to true 16 | Object.keys(query).filter(key => key !== 'q' && query[key] === '') 17 | .forEach(key => query[key] = true) 18 | 19 | return query 20 | } 21 | 22 | module.exports = history => goto$ => { 23 | const history$ = O.from(history(goto$.map(goto => isStr(goto) ? { type: 'push', pathname: goto } : goto))) 24 | .map(loc =>({...loc, pathname: stripBase(loc.pathname), query: parseQuery(loc) })) 25 | 26 | const page$ = history$ 27 | .filter((loc, i) => i == 0 || !loc.state || !loc.state.noRouting) 28 | 29 | function route(path) { 30 | if (!path) return page$ 31 | 32 | const keys=[], re=pathToRegexp(path, keys) 33 | 34 | return page$ 35 | .map(loc => ({ ...loc, matches: loc.pathname.match(re) })) 36 | .filter(loc => !!loc.matches) 37 | .map(loc => ({ ...loc, params: makeObj(keys, loc.matches.slice(1)) })) 38 | } 39 | 40 | route.all$ = history$ 41 | 42 | return route 43 | } 44 | -------------------------------------------------------------------------------- /client/src/driver/search.js: -------------------------------------------------------------------------------- 1 | import request from 'superagent' 2 | import { tryUnconfidentialAddress, isHash256 } from '../util' 3 | import { Observable as O } from '../rxjs' 4 | 5 | const reNumber = /^\d+$/ 6 | , reAddrLike = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/ 7 | , reShortTxOut = /^(\d+)([x:])(\d+)\2(\d+)$/ 8 | , trim = s => s.trim() 9 | , stripUri = s => s.replace(/^(?:bitcoin|liquidnetwork):([^?]+).*/i, '$1') 10 | 11 | export default apiBase => { 12 | const tryResource = path => 13 | request(apiBase + path) 14 | .then(r => r.ok ? path : Promise.reject('invalid status')) 15 | 16 | let matches 17 | 18 | // Accepts a stream of query strings, returns a stream of found resource paths 19 | return query$ => 20 | O.from(query$).map(trim).map(stripUri).flatMap(async query => 21 | 22 | // if its a number, assume its a block height without checking 23 | reNumber.test(query) 24 | ? `/block-height/${query}` 25 | 26 | // if its a 256 bit hash, look it up as a txid or block hash 27 | : isHash256(query) 28 | ? tryResource(`/tx/${query}`) 29 | .catch(_ => tryResource(`/block/${query}`)) 30 | .catch(_ => process.env.IS_ELEMENTS ? tryResource(`/asset/${query}`) : null) 31 | .catch(_ => null) 32 | 33 | // lookup as lightning-style short txout identifier 34 | : (matches = query.match(reShortTxOut)) 35 | ? request(`${apiBase}/block-height/${matches[1]}`) 36 | .then(r => r.ok ? r.text : Promise.reject('invalid reply for block height')) 37 | .then(blockhash => request(`${apiBase}/block/${blockhash}/txid/${matches[3]}`)) 38 | .then(r => r.ok ? r.text : Promise.reject('invalid reply for block txid')) 39 | .then(txid => ({ pathname: `/tx/${txid}`, search: `?output:${matches[4]}` })) 40 | .catch(_ => null) 41 | 42 | // lookup as address if it resembles one 43 | : reAddrLike.test(query) 44 | ? tryResource(`/address/${tryUnconfidentialAddress(query)}`) 45 | // use the user-provided address and not the (potentially) unconfidential one 46 | .then(_ => `/address/${query}`) 47 | .catch(_ => null) 48 | 49 | // @XXX the tx/block/addr resource will be fetched again later for display, 50 | // which is somewhat wasteful but not terribly so due to browser caching. 51 | 52 | : null 53 | ) 54 | .map(result => typeof result == 'string' ? { pathname: result } : result) 55 | .share() 56 | } 57 | -------------------------------------------------------------------------------- /client/src/l10n.js: -------------------------------------------------------------------------------- 1 | import createL10ns from 'basic-l10n' 2 | import browserLanguage from 'in-browser-language' 3 | 4 | const langs = require('../../lang/index') 5 | 6 | // use the plural form as the zero form 7 | Object.entries(langs).forEach(([ lang_id, strs ]) => 8 | Object.entries(strs).forEach(([ str, translation ]) => 9 | Array.isArray(translation) && translation.unshift(translation[1]) 10 | ) 11 | ) 12 | 13 | export default createL10ns(langs, { debug: console.error }) 14 | 15 | Object.entries(exports.default).forEach(([ lang_id, lang_t ]) => { 16 | lang_t.lang_id = lang_id 17 | lang_t.langs = exports.default 18 | }) 19 | 20 | export const defaultLang = process.browser ? browserLanguage.pick(Object.keys(exports.default), 'en') : 'en' 21 | -------------------------------------------------------------------------------- /client/src/lib/deduce-blinded.js: -------------------------------------------------------------------------------- 1 | // Attempt to deduce the blinded input/output based on the available information 2 | export function deduceBlinded(tx) { 3 | if (tx._deduced) return; 4 | tx._deduced = true 5 | 6 | // Find ins/outs with unknown amounts (blinded ant not revealed via the `#blinded` hash fragment) 7 | const unknown_ins = tx.vin.filter(vin => vin.prevout && vin.prevout.value == null) 8 | , unknown_outs = tx.vout.filter(vout => vout.value == null) 9 | 10 | // If the transaction has a single unknown input/output, we can deduce its asset/amount 11 | // based on the other known inputs/outputs. 12 | if (unknown_ins.length + unknown_outs.length == 1) { 13 | 14 | // Keep a per-asset tally of all known input amounts, minus all known output amounts 15 | const totals = new Map 16 | tx.vin.filter(vin => vin.prevout && vin.prevout.value != null) 17 | .forEach(({ prevout }) => 18 | totals.set(prevout.asset, (totals.get(prevout.asset) || 0) + prevout.value)) 19 | tx.vout.filter(vout => vout.value != null) 20 | .forEach(vout => 21 | totals.set(vout.asset, (totals.get(vout.asset) || 0) - vout.value)) 22 | 23 | // There should only be a single asset where the inputs and outputs amounts mismatch, 24 | // which is the asset of the blinded input/output 25 | const remainder = Array.from(totals.entries()).filter(([ asset, value ]) => value != 0) 26 | if (remainder.length != 1) throw new Error('unexpected remainder while deducing blinded tx') 27 | const [ blinded_asset, blinded_value ] = remainder[0] 28 | 29 | // A positive remainder (when known in > known out) is the asset/amount of the unknown blinded output, 30 | // a negative one is the input. 31 | if (blinded_value > 0) { 32 | if (!unknown_outs.length) throw new Error('expected unknown output') 33 | unknown_outs[0].asset = blinded_asset 34 | unknown_outs[0].value = blinded_value 35 | } else { 36 | if (!unknown_ins.length) throw new Error('expected unknown input') 37 | unknown_ins[0].prevout.asset = blinded_asset 38 | unknown_ins[0].prevout.value = blinded_value * -1 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/src/run-browser.js: -------------------------------------------------------------------------------- 1 | import run from '@cycle/rxjs-run' 2 | import { makeHTTPDriver } from '@cycle/http' 3 | import { makeDOMDriver } from '@cycle/dom' 4 | import { makeHistoryDriver, captureClicks } from '@cycle/history' 5 | import makeRouteDriver from './driver/route' 6 | import makeSearchDriver from './driver/search' 7 | import makeScanDriver from './driver/instascan' 8 | 9 | import { Observable as O } from './rxjs' 10 | 11 | import main from './app' 12 | 13 | const apiBase = (process.env.API_URL || '/api').replace(/\/+$/, '') 14 | , webBase = process.env.BASE_HREF || '/' 15 | , initTitle = process.browser ? document.title : process.env.SITE_TITLE 16 | 17 | const titleDriver = title$ => O.from(title$) 18 | .subscribe(title => document.title = title ? `${title} · ${initTitle}` : initTitle) 19 | 20 | const blindingDriver = process.env.IS_ELEMENTS 21 | ? require('./driver/blinding') 22 | : _ => O.empty() 23 | 24 | let storageDriver 25 | try { 26 | localStorage // this will fail if localStorage/cookies is blocked 27 | storageDriver = require('@cycle/storage').default 28 | } catch (e) { 29 | // dummy storage driver. writes are ignored, reads always return null 30 | storageDriver = _ => ({ local: { getItem: key => O.of(null) } }) 31 | } 32 | 33 | run(main, { 34 | DOM: makeDOMDriver('#explorer') 35 | , HTTP: makeHTTPDriver() 36 | , route: makeRouteDriver(captureClicks(makeHistoryDriver({ basename: webBase }))) 37 | , storage: storageDriver 38 | , search: makeSearchDriver(apiBase) 39 | , title: titleDriver 40 | , scanner: makeScanDriver() 41 | , blinding: blindingDriver 42 | }) 43 | -------------------------------------------------------------------------------- /client/src/run-server.js: -------------------------------------------------------------------------------- 1 | import run from '@cycle/rxjs-run' 2 | import { makeHTTPDriver } from '@cycle/http' 3 | import { makeHTMLDriver } from '@cycle/html' 4 | import makeRouteDriver from './driver/route' 5 | import makeSearchDriver from './driver/search' 6 | import { Observable as O } from './rxjs' 7 | 8 | import main from './app' 9 | 10 | const apiBase = (process.env.API_URL || '/api').replace(/\/+$/, '') 11 | 12 | const LOAD_TIMEOUT = process.env.PRERENDER_TIMEOUT || 30000 13 | , ROUTE_TIMEOUT = 50 14 | , INIT_STATE_TIMEOUT = 1000 15 | 16 | // should not be necessary following https://github.com/cyclejs/cyclejs/pull/874 17 | const ModulesForHTML = Object.values(require('snabbdom-to-html/modules')) 18 | 19 | export default function render(pathname, args='', body, locals={}, cb) { 20 | 21 | let lastHtml, lastState, seenLoading=false, called=false 22 | 23 | let timeout = setTimeout(_ => done({ errorCode: 500 }), INIT_STATE_TIMEOUT) 24 | 25 | function done(data) { 26 | if (called) return console.error('html render result() called too many times', pathname, '\n------\ndata: ', data, '\n------\n lastState:', lastState, '\n------\n lastHtml:', lastHtml, '\n\n\n'); 27 | called = true 28 | clearTimeout(timeout) 29 | dispose() 30 | 31 | cb(null, data || { 32 | html: lastHtml 33 | , title: lastState.title 34 | , status: lastState.view == 'notFound' ? 404 35 | : lastState.view == 'error' ? lastState.error.status || 400 36 | : 200 37 | }) 38 | } 39 | 40 | function htmlUpdate(html) { 41 | lastHtml = html 42 | } 43 | function stateUpdate(S) { 44 | if (!lastState) { // the first state 45 | clearTimeout(timeout) 46 | timeout = setTimeout(_ => done(), ROUTE_TIMEOUT) 47 | } 48 | 49 | lastState = S 50 | 51 | if (S.view == 'loading' || S.loading > 0) { 52 | if (!seenLoading) { 53 | seenLoading = true 54 | clearTimeout(timeout) 55 | timeout = setTimeout(_ => done({ errorCode: 504 }), LOAD_TIMEOUT) 56 | } 57 | } 58 | else if (seenLoading) done() 59 | } 60 | 61 | const historyDriver = goto$ => { 62 | O.from(goto$).subscribe(loc => { 63 | done({ redirect: loc.pathname + (loc.search ? '?'+loc.search : '') }) 64 | }) 65 | return O.of({ pathname, search: '?'+args, body }) 66 | } 67 | 68 | const dispose = run(main, { 69 | DOM: makeHTMLDriver(htmlUpdate, { modules: ModulesForHTML }) 70 | , HTTP: makeHTTPDriver() 71 | , route: makeRouteDriver(historyDriver) 72 | , storage: _ => ({ local: { getItem: key => O.of(locals[key]) } }) 73 | , scanner: _ => O.empty() 74 | , search: makeSearchDriver(apiBase) 75 | , state: state$ => O.from(state$).subscribe(stateUpdate) 76 | // unblinding is disabled with server-side rendering 77 | , blinding: _ => O.empty(), 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /client/src/rxjs.js: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs/Observable' 2 | 3 | import 'rxjs/add/observable/empty' 4 | import 'rxjs/add/observable/of' 5 | import 'rxjs/add/observable/merge' 6 | import 'rxjs/add/observable/combineLatest' 7 | import 'rxjs/add/observable/timer' 8 | import 'rxjs/add/observable/fromEvent' 9 | import 'rxjs/add/observable/from' 10 | 11 | import 'rxjs/add/operator/filter' 12 | import 'rxjs/add/operator/map' 13 | import 'rxjs/add/operator/mapTo' 14 | import 'rxjs/add/operator/withLatestFrom' 15 | import 'rxjs/add/operator/merge' 16 | import 'rxjs/add/operator/catch' 17 | import 'rxjs/add/operator/startWith' 18 | import 'rxjs/add/operator/mergeMap' 19 | import 'rxjs/add/operator/scan' 20 | import 'rxjs/add/operator/combineLatest' 21 | import 'rxjs/add/operator/share' 22 | import 'rxjs/add/operator/throttleTime' 23 | import 'rxjs/add/operator/switchMap' 24 | import 'rxjs/add/operator/distinctUntilChanged' 25 | import 'rxjs/add/operator/first' 26 | import 'rxjs/add/operator/skip' 27 | import 'rxjs/add/operator/concat' 28 | import 'rxjs/add/operator/pluck' 29 | import 'rxjs/add/operator/delay' 30 | import 'rxjs/add/operator/shareReplay' 31 | 32 | module.exports = { Observable } 33 | -------------------------------------------------------------------------------- /client/src/views/blocks-all.js: -------------------------------------------------------------------------------- 1 | import Snabbdom from 'snabbdom-pragma' 2 | import layout from './layout' 3 | import { blks } from './blocks' 4 | 5 | const isTouch = process.browser && ('ontouchstart' in window) 6 | 7 | const homeLayout = (body, { t, activeTab, ...S }) => layout( 8 |
9 | { body } 10 |
11 | , { t, isTouch, activeTab, ...S }) 12 | 13 | export const recentBlocks = ({ t, blocks, loading, ...S }) => homeLayout( 14 |
15 | { blks(blocks, false, true, { t, loading, ...S }) } 16 |
17 | , { ...S, t, activeTab: 'recentBlocks' }) 18 | 19 | -------------------------------------------------------------------------------- /client/src/views/blocks.js: -------------------------------------------------------------------------------- 1 | import Snabbdom from 'snabbdom-pragma' 2 | import { formatTime, formatNumber } from './util' 3 | import loader from '../components/loading' 4 | 5 | const staticRoot = process.env.STATIC_ROOT || '' 6 | 7 | export const blks = (blocks, viewMore, loadMore, { t, loading, ...S }) => 8 |
9 | { !blocks ? loader() 10 | : !blocks.length ?

{t`No recent blocks`}

11 | :
12 |

{t`Latest Blocks`}

13 |
14 |
{t`Height`}
15 |
{process.browser ? t`Timestamp` : t`Timestamp (UTC)`}
16 |
{t`Transactions`}
17 |
{t`Size (KB)`}
18 |
{t`Weight (KWU)`}
19 |
20 | { blocks && blocks.map(b => 21 | 30 | )} 31 | {blocks && viewMore ? 32 | 33 | {t`View more blocks`} 34 |
35 |
: ""} 36 | {loadMore ? 37 |
38 |
39 | { loading 40 | ?
{t`Load more`}
{loader("small")}
41 | : pagingNav({ ...S, t }) } 42 |
43 |
44 | : "" } 45 |
46 | } 47 |
48 | 49 | 50 | const pagingNav = ({ nextBlocks, prevBlocks, t }) => 51 | process.browser 52 | 53 | ? nextBlocks != null && 54 |
55 | {t`Load more`} 56 |
57 | 58 | : [ 59 | prevBlocks != null && 60 | 61 |
62 | {t`Newer`} 63 |
64 | , nextBlocks != null && 65 | 66 | {t`Older`} 67 |
68 |
69 | ] 70 | -------------------------------------------------------------------------------- /client/src/views/error.js: -------------------------------------------------------------------------------- 1 | import Snabbdom from 'snabbdom-pragma' 2 | import layout from './layout' 3 | 4 | const formatError = err => 5 | (err.message && err.message.startsWith('Request has been terminated')) 6 | ? 'We encountered an error. Please try again later.' 7 | : (err.status && err.status === 502) 8 | ? 'Esplora is currently unavailable, please try again later.' 9 | : (err.message || err.toString()) 10 | 11 | export const error = ({ t, error, ...S }) => layout(
12 |

{ t(formatError(error)) }

13 |
14 | , { t, ...S }) 15 | 16 | export const notFound = S => error({ ...S, error: 'Page Not Found' }) 17 | -------------------------------------------------------------------------------- /client/src/views/footer.js: -------------------------------------------------------------------------------- 1 | import Snabbdom from 'snabbdom-pragma' 2 | 3 | const staticRoot = process.env.STATIC_ROOT || '' 4 | const links = process.env.FOOTER_LINKS ? JSON.parse(process.env.FOOTER_LINKS) : { [staticRoot+'img/github_blue.png']: 'https://github.com/blockstream/esplora' } 5 | 6 | 7 | export default ({ t, page }) => 8 |
9 |
10 |
11 |
12 |
13 | { !process.browser && Object.entries(page.query).map(([k, v]) => 14 | k != 'lang' && 15 | ) } 16 | 21 | { !process.browser && } 22 |
23 |
24 |
25 | 26 |
27 | { Object.entries(links).map(([ imgSrc, url ]) => 28 | 29 | 30 | 31 | ) } 32 |
33 | 34 | { (process.env.ONION_V3) && 35 |
36 |
37 |
38 | { process.env.ONION_V3 && Onion V3 } 39 |
40 |
41 | } 42 | 43 |
44 |
45 |
46 | { process.env.TERMS && Terms & } 47 | { process.env.PRIVACY && Privacy } 48 |
49 |
{ process.env.SITE_FOOTER || t`Powered by esplora` }
50 |
51 |
52 |
53 |
54 | -------------------------------------------------------------------------------- /client/src/views/home.js: -------------------------------------------------------------------------------- 1 | import Snabbdom from 'snabbdom-pragma' 2 | import layout from './layout' 3 | import { blks } from './blocks' 4 | import { transactions } from './transactions' 5 | 6 | const isTouch = process.browser && ('ontouchstart' in window) 7 | 8 | const homeLayout = (body, { t, activeTab, ...S }) => layout( 9 |
10 | { body } 11 |
12 | , { t, isTouch, activeTab, ...S }) 13 | 14 | export const dashBoard = ({ t, blocks, dashboardState, loading, ...S }) => { 15 | const { dashblocks, dashTxs } = dashboardState || {} 16 | 17 | return (homeLayout( 18 |
19 | { blks( dashblocks, true, false, { t, ...S }) } 20 | {transactions( dashTxs, true, { t } )} 21 | 22 |
23 | , { ...S, t, activeTab: 'dashBoard' }) 24 | )} 25 | -------------------------------------------------------------------------------- /client/src/views/index.js: -------------------------------------------------------------------------------- 1 | export { dashBoard } from './home' 2 | export { default as apiLanding } from './lander' 3 | export { recentBlocks } from './blocks-all' 4 | export { recentTxs } from './transactions-all' 5 | export { default as block } from './block' 6 | export { default as addr } from './addr' 7 | export { default as tx } from './tx' 8 | export { default as pushtx } from './pushtx' 9 | export { default as scan } from './scan' 10 | export { default as mempool } from './mempool' 11 | export { default as loading } from './loading' 12 | export { error, notFound } from './error' 13 | 14 | // Elements 15 | if (process.env.IS_ELEMENTS) { 16 | exports.asset = require('./asset').default 17 | 18 | if (process.env.ASSET_MAP_URL) { 19 | exports.assetList = require('./asset-list').default 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/src/views/layout.js: -------------------------------------------------------------------------------- 1 | import Snabbdom from 'snabbdom-pragma' 2 | import navbar from './navbar' 3 | import footer from './footer' 4 | import subnav from './sub-navbar' 5 | 6 | export default (body, opt) => 7 |
8 |
9 | { navbar(opt) } 10 | {subnav(opt.t, opt.isTouch, opt.activeTab)} 11 | { body } 12 |
13 | { footer(opt) } 14 |
15 | -------------------------------------------------------------------------------- /client/src/views/loading.js: -------------------------------------------------------------------------------- 1 | import Snabbdom from 'snabbdom-pragma' 2 | import layout from './layout' 3 | 4 | 5 | export default ({ t, ...S }) => layout(
6 |
7 |
8 |
{t`Loading`}
9 |
10 |
, { t, ...S }) 11 | -------------------------------------------------------------------------------- /client/src/views/mempool.js: -------------------------------------------------------------------------------- 1 | import Snabbdom from 'snabbdom-pragma' 2 | import { getMempoolDepth, squashFeeHistogram, feerateCutoff } from '../lib/fees' 3 | import { formatSat, formatVMB } from './util' 4 | import layout from './layout' 5 | import search from './search' 6 | 7 | let squashed 8 | 9 | export default ({ t, mempool, feeEst, ...S }) => mempool && feeEst && layout( 10 |
11 |
12 |
13 |
14 |

{t`Mempool`}

15 |
16 |
17 |
18 |
{t`Total transactions`}
19 |
{mempool.count}
20 |
21 |
22 |
{t`Total fees`}
23 |
{formatSat(mempool.total_fee)}
24 |
25 |
26 |
{t`Total size`}
27 |
{formatVMB(mempool.vsize)}
28 |
29 |
30 |
31 |
32 |
33 |
34 | { mempool.fee_histogram.length > 0 && 35 |
36 |

Fee rate distribution

37 | { squashed = squashFeeHistogram(mempool.fee_histogram), squashed.map(([ rangeStart, binSize ], i) => binSize > 0 && 38 |
39 | {`${rangeStart.toFixed(1)}${i == 0 ? '+' : ' - '+squashed[i-1][0].toFixed(1)}`} 40 | {formatVMB(binSize)} 41 |
42 | )} 43 | {t`sat/vbyte`} 44 |
45 | } 46 | 47 | { !!Object.keys(feeEst).length && 48 |
49 |

Fee rate estimates

50 | 51 | 52 | { sortEst(feeEst).map(([ target, feerate ]) => 53 | 54 | )} 55 |
Targetsat/vBMempool depth
{t`${target} blocks`}{feerate.toFixed(2)}{t`${formatVMB(getMempoolDepth(mempool.fee_histogram, feerate))} from tip`}
56 |
57 | } 58 |
59 | 60 |
61 |
62 | , { ...S, t, mempool, feeEst }) 63 | 64 | const sortEst = feeEst => Object.entries(feeEst).sort((a, b) => a[0]-b[0]) 65 | -------------------------------------------------------------------------------- /client/src/views/navbar-menu.js: -------------------------------------------------------------------------------- 1 | import Snabbdom from 'snabbdom-pragma' 2 | import navToggle from './nav-toggle' 3 | 4 | 5 | const items = process.env.MENU_ITEMS && JSON.parse(process.env.MENU_ITEMS) 6 | , active = process.env.MENU_ACTIVE 7 | 8 | const staticRoot = process.env.STATIC_ROOT || '' 9 | 10 | export default ({ t, theme, page }) => 11 | 12 |
13 | 23 | { process.env.NAVBAR_HTML ? navToggle(t, theme, page) : "" } 24 |
-------------------------------------------------------------------------------- /client/src/views/navbar.js: -------------------------------------------------------------------------------- 1 | import Snabbdom from 'snabbdom-pragma' 2 | import menu from './navbar-menu' 3 | 4 | export default S => 5 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /client/src/views/pushtx.js: -------------------------------------------------------------------------------- 1 | import Snabbdom from 'snabbdom-pragma' 2 | import layout from './layout' 3 | 4 | export default ({ t, ...S }) => layout( 5 |
6 |
7 |
8 |
9 |
10 | 11 | 12 |
13 | 14 |
15 |
16 |
17 |
18 | , { t, ...S }) 19 | -------------------------------------------------------------------------------- /client/src/views/scan.js: -------------------------------------------------------------------------------- 1 | import Snabbdom from 'snabbdom-pragma' 2 | import layout from './layout' 3 | 4 | export default ({ t, ...S }) => layout( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {t`Cancel`} 13 |
14 |
15 |
16 | , { ...S, t }) 17 | -------------------------------------------------------------------------------- /client/src/views/search.js: -------------------------------------------------------------------------------- 1 | import Snabbdom from 'snabbdom-pragma' 2 | 3 | const staticRoot = process.env.STATIC_ROOT || '' 4 | const hasCam = process.browser && navigator.mediaDevices && navigator.mediaDevices.getUserMedia 5 | 6 | export default ({ t, klass, autofocus }) => 7 |
8 |
9 | 19 | { hasCam ? : "" } 20 | 21 |
22 |
23 | -------------------------------------------------------------------------------- /client/src/views/sub-navbar.js: -------------------------------------------------------------------------------- 1 | import Snabbdom from 'snabbdom-pragma' 2 | import search from './search' 3 | 4 | export default ( t, isTouch, activeTab) => 5 |
6 |
7 |
8 | Dashboard 9 | Blocks 10 | Transactions 11 | { process.env.IS_ELEMENTS ? Assets : "" } 12 | Explorer API 13 |
14 | 15 | { search({ t, autofocus: !isTouch }) } 16 |
17 |
-------------------------------------------------------------------------------- /client/src/views/transactions-all.js: -------------------------------------------------------------------------------- 1 | import Snabbdom from 'snabbdom-pragma' 2 | import layout from './layout' 3 | import { transactions } from './transactions' 4 | 5 | const isTouch = process.browser && ('ontouchstart' in window) 6 | 7 | const homeLayout = (body, { t, activeTab, ...S }) => layout( 8 |
9 | { body } 10 |
11 | , { t, isTouch, activeTab, ...S }) 12 | 13 | export const recentTxs = ({ mempoolRecent, t, ...S }) => homeLayout( 14 |
15 | {transactions( mempoolRecent, false, { t, ...S })} 16 |
17 | , { ...S, t, activeTab: 'recentTxs' }) 18 | -------------------------------------------------------------------------------- /client/src/views/transactions.js: -------------------------------------------------------------------------------- 1 | 2 | import Snabbdom from 'snabbdom-pragma' 3 | import { formatSat, formatNumber } from './util' 4 | import loader from '../components/loading' 5 | 6 | const staticRoot = process.env.STATIC_ROOT || '' 7 | 8 | export const transactions = (txs, viewMore, { t } ) => 9 |
10 | { !txs ? loader() 11 | : !txs.length ?

{t`No recent transactions`}

12 | :
13 |

{t`Latest Transactions`}

14 |
15 |
{t`Transaction ID`}
16 | { txs[0].value != null &&
{t`Value`}
} 17 |
{t`Size`}
18 |
{t`Fee`}
19 |
20 | {txs.map(txOverview => { const feerate = txOverview.fee/txOverview.vsize; return ( 21 | 29 | )})} 30 | 31 | {txs && viewMore ? 32 | 33 | {t`View more transactions`} 34 |
35 |
: ""} 36 |
37 | } 38 |
39 | 40 | -------------------------------------------------------------------------------- /client/src/views/tx-segwit-gains.js: -------------------------------------------------------------------------------- 1 | import Snabbdom from 'snabbdom-pragma' 2 | 3 | const percent = num => `${num >= 0.01 ? Math.round(num*100) : (num*100).toFixed(2)}%` 4 | 5 | const makeMessage = ({ realizedGains, potentialBech32Gains, potentialP2shGains }, t) => 6 | (realizedGains && !potentialBech32Gains) 7 | ? [ 'success', t`This transaction saved ${percent(realizedGains)} on fees by upgrading to native SegWit-Bech32` ] 8 | : (realizedGains && potentialBech32Gains) 9 | ? [ 'warning', t`This transaction saved ${percent(realizedGains)} on fees by upgrading to SegWit and could save ${percent(potentialBech32Gains)} more by fully upgrading to native SegWit-Bech32` ] 10 | : (potentialP2shGains || potentialBech32Gains) 11 | ? [ 'danger', t`This transaction could save ${percent(potentialBech32Gains)} on fees by upgrading to native SegWit-Bech32 or ${percent(potentialP2shGains)} by upgrading to SegWit-P2SH` ] 12 | : null 13 | 14 | export default (gains, t) => { 15 | const msg = makeMessage(gains, t) 16 | return msg ? {msg[1]} : null 17 | } 18 | -------------------------------------------------------------------------------- /contrib/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM blockstream/esplora-base:latest AS build 2 | 3 | FROM debian:bookworm-slim 4 | 5 | COPY --from=build /srv/explorer /srv/explorer 6 | COPY --from=build /srv/wally_wasm /srv/wally_wasm 7 | COPY --from=build /root/.nvm /root/.nvm 8 | 9 | RUN apt-get -yqq update \ 10 | && apt-get -yqq upgrade \ 11 | && apt-get -yqq install nginx libnginx-mod-http-lua tor git curl runit procps socat gpg 12 | 13 | RUN mkdir -p /srv/explorer/static 14 | 15 | COPY ./ /srv/explorer/source 16 | 17 | ARG FOOT_HTML 18 | 19 | WORKDIR /srv/explorer/source 20 | 21 | SHELL ["/bin/bash", "-c"] 22 | 23 | RUN source /root/.nvm/nvm.sh \ 24 | && npm install && (cd prerender-server && npm run dist) \ 25 | && DEST=/srv/explorer/static/bitcoin-mainnet \ 26 | npm run dist -- bitcoin-mainnet \ 27 | && DEST=/srv/explorer/static/bitcoin-testnet \ 28 | npm run dist -- bitcoin-testnet \ 29 | && DEST=/srv/explorer/static/bitcoin-signet \ 30 | npm run dist -- bitcoin-signet \ 31 | && DEST=/srv/explorer/static/bitcoin-regtest \ 32 | npm run dist -- bitcoin-regtest \ 33 | && DEST=/srv/explorer/static/liquid-mainnet \ 34 | npm run dist -- liquid-mainnet \ 35 | && DEST=/srv/explorer/static/liquid-testnet \ 36 | npm run dist -- liquid-testnet \ 37 | && DEST=/srv/explorer/static/liquid-regtest \ 38 | npm run dist -- liquid-regtest \ 39 | && DEST=/srv/explorer/static/bitcoin-mainnet-blockstream \ 40 | npm run dist -- bitcoin-mainnet blockstream \ 41 | && DEST=/srv/explorer/static/bitcoin-testnet-blockstream \ 42 | npm run dist -- bitcoin-testnet blockstream \ 43 | && DEST=/srv/explorer/static/bitcoin-signet-blockstream \ 44 | npm run dist -- bitcoin-signet blockstream \ 45 | && DEST=/srv/explorer/static/bitcoin-regtest-blockstream \ 46 | npm run dist -- bitcoin-regtest blockstream \ 47 | && DEST=/srv/explorer/static/liquid-mainnet-blockstream \ 48 | npm run dist -- liquid-mainnet blockstream \ 49 | && DEST=/srv/explorer/static/liquid-testnet-blockstream \ 50 | npm run dist -- liquid-testnet blockstream \ 51 | && DEST=/srv/explorer/static/liquid-regtest-blockstream \ 52 | npm run dist -- liquid-regtest blockstream 53 | 54 | # symlink the libwally wasm files into liquid's www directories (for client-side unblinding) 55 | RUN for dir in /srv/explorer/static/liquid*; do ln -s /srv/wally_wasm $dir/libwally; done 56 | 57 | # configuration 58 | RUN cp /srv/explorer/source/run.sh /srv/explorer/ 59 | 60 | # cleanup 61 | RUN apt-get --auto-remove remove -yqq --purge manpages \ 62 | && apt-get clean \ 63 | && apt-get autoclean \ 64 | && rm -rf /usr/share/doc* /usr/share/man /usr/share/postgresql/*/man /var/lib/apt/lists/* /var/cache/* /tmp/* /root/.cache /*.deb /root/.cargo 65 | 66 | WORKDIR /srv/explorer 67 | -------------------------------------------------------------------------------- /contrib/asset_registry_pubkey.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mDMEXO8+ZRYJKwYBBAHaRw8BAQdAuOmTOyYQvFquAX5qAwgzuyrK7z3OAJfqxi44 4 | dHobhiq0GSAoTGlxdWlkIHJlZ2lzdHJ5IGFzc2V0cymIlgQTFggAPgIbAwULCQgH 5 | AgYVCAkKCwIEFgIDAQIeAQIXgBYhBKHfg3cPKVSCKBcNY9urujrVJayhBQJoLpk+ 6 | BQkPDvDZAAoJENurujrVJayhATkA/1KJUequVNe4j3SHBorCPgDTi6tAPqOcdYNA 7 | vg6u2G44AP0R+2PnBjJ1FDallgUbhgGl0cDyPKBgjXl5R4M+WDN9Cg== 8 | =PW6p 9 | -----END PGP PUBLIC KEY BLOCK----- 10 | -------------------------------------------------------------------------------- /contrib/asset_registry_testnet_pubkey.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mDMEYVXhoxYJKwYBBAHaRw8BAQdACf/c/CgDnpoqbXA2njco2FyvN8dno5BvleJ8 4 | AFymI620HUxpcXVpZCBUZXN0bmV0IEFzc2V0IFJlZ2lzdHJ5iJYEExYIAD4CGwMF 5 | CwkIBwMFFQoJCAsFFgIDAQACHgECF4AWIQTNdnlwTMzpaxREZ5SzJgag+fNxNgUC 6 | Zuv6RQUJCzmzIgAKCRCzJgag+fNxNjrYAP90m6itQkX6JxtvkBqk512y8VWXwDik 7 | uM1laE1IXoTlCAEAv1cmVAhRCsKGxblGbncypQKm2otNIah2/3l9h5bZZAo= 8 | =sNmd 9 | -----END PGP PUBLIC KEY BLOCK----- 10 | -------------------------------------------------------------------------------- /contrib/bitcoin-mainnet-explorer.conf.in: -------------------------------------------------------------------------------- 1 | [main] 2 | server=1 3 | peerbloomfilters=0 4 | enforcenodebloom=1 5 | disablewallet=1 6 | listenonion=1 7 | listen=1 8 | mempoolfullrbf=1 9 | blocknotify=pkill -USR1 electrs 10 | maxmempool=1000 11 | -------------------------------------------------------------------------------- /contrib/bitcoin-mainnet-pruned-for-liquid.conf.in: -------------------------------------------------------------------------------- 1 | [main] 2 | server=1 3 | peerbloomfilters=0 4 | enforcenodebloom=1 5 | disablewallet=1 6 | prune=550 7 | blocksonly=1 8 | persistmempool=0 9 | listen=0 10 | -------------------------------------------------------------------------------- /contrib/bitcoin-regtest-explorer.conf.in: -------------------------------------------------------------------------------- 1 | regtest=1 2 | [regtest] 3 | server=1 4 | listen=1 5 | mempoolfullrbf=1 6 | blocknotify=pkill -USR1 electrs 7 | fallbackfee=0.00001 8 | maxmempool=1000 9 | -------------------------------------------------------------------------------- /contrib/bitcoin-signet-explorer.conf.in: -------------------------------------------------------------------------------- 1 | signet=1 2 | [signet] 3 | server=1 4 | peerbloomfilters=0 5 | enforcenodebloom=1 6 | disablewallet=1 7 | listenonion=1 8 | listen=1 9 | mempoolfullrbf=1 10 | blocknotify=pkill -USR1 electrs 11 | maxmempool=1000 12 | -------------------------------------------------------------------------------- /contrib/bitcoin-testnet-explorer.conf.in: -------------------------------------------------------------------------------- 1 | testnet=1 2 | [test] 3 | server=1 4 | peerbloomfilters=0 5 | enforcenodebloom=1 6 | disablewallet=1 7 | listenonion=1 8 | listen=1 9 | mempoolfullrbf=1 10 | blocknotify=pkill -USR1 electrs 11 | maxmempool=1000 12 | -------------------------------------------------------------------------------- /contrib/bitcoind-wait-sync.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | $1 -rpcwait getblockchaininfo > /dev/null 5 | 6 | while :; do 7 | chaininfo=$($1 getblockchaininfo) 8 | headers=$(echo "$chaininfo" | egrep -o '"headers": [0-9]+' | cut -d' ' -f2) 9 | blocks=$(echo "$chaininfo" | egrep -o '"blocks": [0-9]+' | cut -d' ' -f2) 10 | ibd=$(echo "$chaininfo" | egrep -o '"initialblockdownload": [a-z]+' | cut -d' ' -f2) 11 | 12 | echo "$blocks blocks of $headers headers (ibd: $ibd)" 13 | 14 | if [[ $ibd == "false" && $headers == $blocks ]]; then 15 | echo "done syncing" 16 | break 17 | fi 18 | 19 | sleep 30 20 | done 21 | -------------------------------------------------------------------------------- /contrib/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | esplora: 3 | image: blockstream/esplora 4 | build: 5 | context: ../ 6 | dockerfile: contrib/Dockerfile 7 | restart: always 8 | ports: 9 | - "50001:50001" 10 | - "8080:8080" 11 | - "80:80" 12 | command: 13 | - bash 14 | - -c 15 | - '/srv/explorer/run.sh bitcoin-regtest explorer nonverbose' -------------------------------------------------------------------------------- /contrib/electrum-announce.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | network=$1 4 | hosts=$2 5 | server_version="electrs-esplora ${3:-0.4.1}" 6 | 7 | if [[ $network != "mainnet" && $network != "testnet" ]]; then 8 | echo >&2 Invalid network 9 | exit 1 10 | fi 11 | if [ -z "$hosts" ]; then 12 | echo >&2 Missing hosts 13 | exit 1 14 | fi 15 | 16 | servers_file=servers-$network.txt 17 | 18 | if [ ! -f $servers_file ]; then 19 | echo >&2 Missing servers file 20 | exit 1 21 | fi 22 | 23 | if [ $network == "mainnet" ]; then 24 | default_tcp_port=50001 25 | default_ssl_port=50002 26 | genesis_hash=000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f 27 | else 28 | default_tcp_port=51001 29 | default_ssl_port=51002 30 | genesis_hash=000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943 31 | fi 32 | 33 | features='{"hosts":'$hosts',"server_version":"'$server_version'","genesis_hash":"'$genesis_hash'","protocol_min":"1.4","protocol_max":"1.4","hash_function":"sha256","pruning":null}' 34 | 35 | send_rpc() { 36 | host=$1; port=$2; transport=$3; method=$4; params=$5 37 | payload='{"jsonrpc":"2.0","id":1,"method":"'$method'","params":['$params']}' 38 | echo sending $method $params to $host:$port:$transport >&2 39 | 40 | torify=$([[ $host == *".onion" ]] && echo torsocks || echo '') 41 | 42 | if [ $transport == "t" ]; then 43 | echo "$payload" | $torify nc -q2 $host $port 44 | else 45 | echo "$payload" | $torify socat -T2 -t2 openssl:$host:$port,verify=0 stdio 46 | fi 47 | } 48 | 49 | cat $servers_file | sort | uniq | sponge $servers_file 50 | 51 | servers=`shuf $servers_file` 52 | for server in $servers; do 53 | echo Fetching servers from $server 54 | IFS=',' read host port transport <<< $server 55 | peers=`send_rpc $host $port $transport server.peers.subscribe` 56 | echo "$peers" | jq -c '.result[]' | while read peer; do 57 | peer_ip=`jq -r .[0] <<< $peer` 58 | peer_opt=`jq -r .[2] <<< $peer` 59 | peer_ssl=`jq -r '.[] | select(startswith("s")) | (if length > 1 then .[1:] else '$default_ssl_port' end + ",s")' <<< $peer_opt` 60 | peer_tcp=`jq -r '.[] | select(startswith("t")) | (if length > 1 then .[1:] else '$default_tcp_port' end + ",t")' <<< $peer_opt` 61 | echo $peer_ip,${peer_ssl:-${peer_tcp:-$default_tcp_port,t}} | sed -r 's/[^\d.:,ts]//g' | tee -a $servers_file 62 | done 63 | done 64 | 65 | cat $servers_file | sort | uniq | sponge $servers_file 66 | 67 | shuf $servers_file | while read server; do 68 | echo Announcing to $server 69 | IFS=':' read host port transport <<< $server 70 | send_rpc $host $port $transport server.add_peer "$features" 71 | done 72 | -------------------------------------------------------------------------------- /contrib/liquid-mainnet-explorer.conf.in: -------------------------------------------------------------------------------- 1 | server=1 2 | peerbloomfilters=0 3 | enforcenodebloom=1 4 | disablewallet=1 5 | validatepegin=0 6 | mainchainrpccookiefile=/data/bitcoin/.cookie 7 | listenonion=1 8 | listen=1 9 | blocknotify=pkill -USR1 electrs 10 | blockmintxfee=0.00000099 11 | minrelaytxfee=0.00000099 12 | # https://github.com/ElementsProject/elements/blob/master/contrib/seeds/liquid/nodes_main.txt 13 | addnode=35.196.16.254:7042 14 | addnode=35.237.176.63:7042 15 | addnode=35.237.81.14:7042 16 | addnode=35.237.147.21:7042 17 | addnode=35.227.95.109:7042 18 | addnode=35.231.141.173:7042 19 | addnode=104.196.48.184:7042 20 | addnode=35.231.10.147:7042 21 | -------------------------------------------------------------------------------- /contrib/liquid-mainnet-private-bridge-torrc: -------------------------------------------------------------------------------- 1 | RunAsDaemon 0 2 | SOCKSPort 9050 3 | LongLivedPorts 10100 #Prefer high-uptime nodes for liquid-daemon connections 4 | -------------------------------------------------------------------------------- /contrib/liquid-mainnet-private-bridge.conf.in: -------------------------------------------------------------------------------- 1 | server=1 2 | peerbloomfilters=0 3 | enforcenodebloom=1 4 | disablewallet=1 5 | listenonion=0 6 | proxy=127.0.0.1:9050 7 | onlynet=onion 8 | listen=0 9 | mainchainrpccookiefile=/data/bitcoin/.cookie 10 | blockmintxfee=0.00000099 11 | minrelaytxfee=0.00000099 12 | -------------------------------------------------------------------------------- /contrib/liquid-mainnet-public-bridge-torrc: -------------------------------------------------------------------------------- 1 | RunAsDaemon 0 2 | ControlPort 9051 3 | SOCKSPort 9050 4 | -------------------------------------------------------------------------------- /contrib/liquid-mainnet-public-bridge.conf.in: -------------------------------------------------------------------------------- 1 | server=1 2 | peerbloomfilters=0 3 | enforcenodebloom=1 4 | disablewallet=1 5 | mainchainrpccookiefile=/data/bitcoin/.cookie 6 | blockmintxfee=0.00000099 7 | minrelaytxfee=0.00000099 8 | -------------------------------------------------------------------------------- /contrib/liquid-regtest-explorer.conf.in: -------------------------------------------------------------------------------- 1 | chain=liquidregtest 2 | server=1 3 | validatepegin=0 4 | initialfreecoins=2100000000000000 5 | blocknotify=pkill -USR1 electrs 6 | fallbackfee=0.000001 7 | blockmintxfee=0.00000099 8 | minrelaytxfee=0.00000099 9 | -------------------------------------------------------------------------------- /contrib/liquid-testnet-explorer.conf.in: -------------------------------------------------------------------------------- 1 | chain=liquidtestnet 2 | # Liquid Testnet (liquidtestnet) settings: 3 | [liquidtestnet] 4 | 5 | # General settings: 6 | addnode=liquid-testnet.blockstream.com:18892 7 | addnode=liquidtestnet.com:18891 8 | addnode=liquidcert.com:18886 9 | addnode=liquid.network:18444 10 | fallbackfee=0.00000100 11 | listenonion=1 12 | listen=1 13 | disablewallet=1 14 | blocknotify=pkill -USR1 electrs 15 | rpcport=7040 16 | -------------------------------------------------------------------------------- /contrib/nginx-liquid-assets.conf.in: -------------------------------------------------------------------------------- 1 | location /{NGINX_PATH}_data/assets.minimal.json { 2 | alias /srv/liquid-assets-db/index.minimal.json; 3 | } 4 | -------------------------------------------------------------------------------- /contrib/nginx-sync.conf.in: -------------------------------------------------------------------------------- 1 | # Dump the current mempool and return it 2 | location = /{NGINX_PATH}_sync/mempool { 3 | auth_basic "private"; 4 | auth_basic_user_file /srv/explorer/htpasswd; 5 | 6 | content_by_lua_block { 7 | os.execute("/usr/bin/cli savemempool"); 8 | ngx.exec("/{NGINX_PATH}_sync/mempool.dat"); 9 | } 10 | } 11 | 12 | # Return mempool.dat without dumping it first, used as an internal redirect 13 | # from the location block above 14 | location = /{NGINX_PATH}_sync/mempool.dat { 15 | auth_basic "private"; 16 | auth_basic_user_file /srv/explorer/htpasswd; 17 | alias {DAEMON_DIR}/mempool.dat; 18 | } -------------------------------------------------------------------------------- /contrib/runit_boot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | shutdown() { 4 | echo "shutting down the container" 5 | 6 | # first shut down socat (avoid "Iterrupted by signal 15" error) 7 | sv -w "$SVWAIT" force-stop socat 8 | 9 | # next shut down electrs (avoid long wait + losing blocks in bitcoind) 10 | sv -w "$SVWAIT" force-stop electrs 11 | 12 | # next shut down bitcoin (longer timeout to allow for a clean shutdown) 13 | sv -w "$BCWAIT" force-stop bitcoin 14 | 15 | # then shutdown any other service started by runit 16 | for _srv in $(ls -1 /etc/service); do 17 | sv -w "$SVWAIT" force-stop "$_srv" 18 | done 19 | 20 | # shutdown runsvdir command 21 | kill -HUP "$RUNSVDIR" 22 | wait "$RUNSVDIR" 23 | 24 | # give processes time to stop 25 | sleep 0.5 26 | 27 | # kill any other processes still running in the container 28 | for _pid in $(ps -eo pid | grep -v PID | tr -d ' ' | grep -v '^1$' | head -n -6); do 29 | timeout 5 /bin/sh -c "kill $_pid && wait $_pid || kill -9 $_pid" 30 | done 31 | exit 32 | } 33 | 34 | # store environment variables 35 | export > /etc/envvars 36 | 37 | PATH=/usr/local/bin:/usr/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/X11R6/bin 38 | SVWAIT=60 # wait process to end up to SVWAIT seconds before sending kill signal 39 | BCWAIT=300 # custom timeout for bitcoind before sending kill signal 40 | 41 | # run all scripts in the run_once folder 42 | /bin/run-parts /etc/run_once 43 | 44 | exec env - PATH=$PATH runsvdir -P /etc/service & 45 | 46 | RUNSVDIR=$! 47 | echo "Started runsvdir, PID is $RUNSVDIR" 48 | echo "wait for processes to start...." 49 | 50 | sleep 5 51 | for _srv in $(ls -1 /etc/service); do 52 | sv status "$_srv" 53 | done 54 | 55 | # catch shutdown signals 56 | trap shutdown SIGTERM SIGHUP SIGQUIT SIGINT 57 | wait $RUNSVDIR 58 | 59 | shutdown 60 | -------------------------------------------------------------------------------- /contrib/runits/bitcoin_for_liquid-log-config.runit: -------------------------------------------------------------------------------- 1 | u127.0.0.1:23394 2 | p1- 3 | -------------------------------------------------------------------------------- /contrib/runits/bitcoin_for_liquid-log.runit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec svlogd /data/logs/bitcoin 3 | -------------------------------------------------------------------------------- /contrib/runits/bitcoin_for_liquid.runit: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | exec 2>&1 4 | exec /srv/explorer/bitcoin/bin/bitcoind -conf=/data/.bitcoin.conf -datadir=/data/bitcoin 5 | -------------------------------------------------------------------------------- /contrib/runits/electrs-log-config.runit: -------------------------------------------------------------------------------- 1 | u127.0.0.1:23394 2 | p2- 3 | -------------------------------------------------------------------------------- /contrib/runits/electrs-log.runit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec svlogd /data/logs/electrs 3 | -------------------------------------------------------------------------------- /contrib/runits/electrs.runit: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | {ELECTRS_BACKTRACE} 4 | exec 2>&1 5 | exec /srv/explorer/electrs_{DAEMON}/bin/electrs \ 6 | --timestamp --http-addr 127.0.0.1:3000 \ 7 | --network {ELECTRS_NETWORK} {PARENT_NETWORK} \ 8 | --daemon-dir /data/{DAEMON} --monitoring-addr 0.0.0.0:4224 \ 9 | --electrum-rpc-addr 0.0.0.0:50001 \ 10 | --electrum-txs-limit 100000 \ 11 | --db-dir /data/electrs_{DAEMON}_db/{NETWORK} \ 12 | --tor-proxy 127.0.0.1:9050 \ 13 | {ELECTRS_ARGS} 14 | -------------------------------------------------------------------------------- /contrib/runits/liquid-assets-poller-log-config.runit: -------------------------------------------------------------------------------- 1 | u127.0.0.1:23394 2 | p7- 3 | -------------------------------------------------------------------------------- /contrib/runits/liquid-assets-poller-log.runit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec svlogd /data/logs/poller 3 | -------------------------------------------------------------------------------- /contrib/runits/liquid-assets-poller.runit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | export TZ=UTC 5 | 6 | # Setup a "gitpgp" command to only accept keys from a local keyring file 7 | # https://tribut.de/blog/git-commit-signatures-trusted-keys 8 | if [ ! -f /usr/bin/gitgpg-assets ]; then 9 | echo -e '#!/bin/sh\nexec gpg --no-default-keyring --keyring=./asset-signing-keyring.gpg "$@"' > /usr/bin/gitgpg-assets 10 | chmod +x /usr/bin/gitgpg-assets 11 | fi 12 | 13 | # Clone repo 14 | if [ ! -d /srv/liquid-assets-db ]; then 15 | git clone -c gpg.program=gitgpg-assets --no-checkout {ASSETS_GIT} /srv/liquid-assets-db --depth 1 16 | fi 17 | 18 | cd /srv/liquid-assets-db 19 | 20 | # Create the local keyring file with just the assets db signing key 21 | gitgpg-assets --import {ASSETS_GPG} 22 | 23 | # Mark the key as trusted 24 | KEYID=`gitgpg-assets --list-keys --with-colons | awk -F: '/^pub:/ { print $5 }'` 25 | echo -e "5\ny\nquit\n" | gitgpg-assets --batch --command-fd 0 --expert --edit-key $KEYID trust 26 | 27 | # Verify and do an initial checkout 28 | git verify-commit master 29 | git checkout master 30 | 31 | # Update periodically 32 | while :; do 33 | # Update every 3 minutes, but always on every 3rd round minute to keep updates in sync across servers 34 | sleep $((180 - $(date +%s) % 180)) 35 | 36 | git fetch 37 | git pull --verify-signatures --ff-only || true 38 | done 39 | -------------------------------------------------------------------------------- /contrib/runits/nginx-log-config.runit: -------------------------------------------------------------------------------- 1 | u127.0.0.1:23394 2 | p3- 3 | -------------------------------------------------------------------------------- /contrib/runits/nginx-log.runit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec svlogd /data/logs/nginx 3 | -------------------------------------------------------------------------------- /contrib/runits/nginx.runit: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | exec 2>&1 4 | exec /usr/sbin/nginx -g "daemon off;" 5 | -------------------------------------------------------------------------------- /contrib/runits/nodedaemon-log-config.runit: -------------------------------------------------------------------------------- 1 | u127.0.0.1:23394 2 | p4- 3 | -------------------------------------------------------------------------------- /contrib/runits/nodedaemon-log.runit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec svlogd /data/logs/nodedaemon 3 | -------------------------------------------------------------------------------- /contrib/runits/nodedaemon.runit: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | exec 2>&1 4 | exec /srv/explorer/{DAEMON}/bin/{DAEMON}d -conf=/data/.{DAEMON}.conf -datadir=/data/{DAEMON} 5 | -------------------------------------------------------------------------------- /contrib/runits/prerenderer-log-config.runit: -------------------------------------------------------------------------------- 1 | u127.0.0.1:23394 2 | p5- 3 | -------------------------------------------------------------------------------- /contrib/runits/prerenderer-log.runit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec svlogd /data/logs/prerenderer 3 | -------------------------------------------------------------------------------- /contrib/runits/prerenderer.runit: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | export STATIC_ROOT=/{NGINX_PATH} 4 | export BASE_HREF=/{NGINX_PATH}nojs/ 5 | export NODE_ENV=${NODE_ENV:-production} 6 | export API_URL=http://127.0.0.1/{NGINX_PATH}api 7 | export TZ=UTC 8 | export SOCKET_PATH=/var/prerender-http.sock 9 | 10 | exec 2>&1 11 | exec /bin/bash -c 'source /root/.nvm/nvm.sh && cd /srv/explorer/source && npm run prerender-server -- --cluster {FLAVOR}' 12 | -------------------------------------------------------------------------------- /contrib/runits/socat.runit: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | exec socat UDP-RECV:23394 stderr 4 | -------------------------------------------------------------------------------- /contrib/runits/tor-log-config.runit: -------------------------------------------------------------------------------- 1 | u127.0.0.1:23394 2 | p6- 3 | -------------------------------------------------------------------------------- /contrib/runits/tor-log.runit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec svlogd /data/logs/tor 3 | -------------------------------------------------------------------------------- /contrib/runits/tor.runit: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | exec 2>&1 4 | exec /usr/bin/tor 5 | -------------------------------------------------------------------------------- /contrib/runits/websocket-log-config.runit: -------------------------------------------------------------------------------- 1 | u127.0.0.1:23394 2 | p8- 3 | -------------------------------------------------------------------------------- /contrib/runits/websocket-log.runit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec svlogd /data/logs/websocket 3 | -------------------------------------------------------------------------------- /contrib/runits/websocket.runit: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | exec 2>&1 3 | exec /srv/explorer/websocat/bin/websocat -E --linemode-strip-newlines l-ws-unix:/var/electrum-websocket.sock msg2line:tcp:127.0.0.1:50001 4 | -------------------------------------------------------------------------------- /contrib/sync-mempool.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source="$1" 4 | dest="$2" 5 | 6 | curl -s $source/mempool/txids | jq -r .[] | while read txid; do 7 | echo pushing $txid 8 | curl -s $dest/tx -d $(curl -s $source/tx/$txid/hex) 9 | echo 10 | done 11 | -------------------------------------------------------------------------------- /dev-server.js: -------------------------------------------------------------------------------- 1 | import fs, { promises as fsp } from 'fs' 2 | import pug from 'pug' 3 | import pathu from 'path' 4 | import glob from 'glob' 5 | import express from 'express' 6 | import browserify from 'browserify-middleware' 7 | import cssjanus from 'cssjanus' 8 | 9 | const rpath = p => pathu.join(__dirname, p) 10 | 11 | const app = express() 12 | 13 | app.engine('pug', pug.__express) 14 | 15 | app.use(require('morgan')('dev')) 16 | 17 | if (process.env.CORS_ALLOW) { 18 | app.use((req, res, next) => { 19 | res.set('Access-Control-Allow-Origin', process.env.CORS_ALLOW) 20 | next() 21 | }) 22 | } 23 | 24 | if (process.env.NOSCRIPT_REDIR_BASE) { 25 | app.use((req, res, next) => { 26 | if (req.query.nojs != null) return res.redirect(303, process.env.NOSCRIPT_REDIR_BASE+req.path) 27 | next() 28 | }) 29 | } 30 | 31 | const custom_assets = (process.env.CUSTOM_ASSETS||'').split(/ +/).filter(Boolean) 32 | , custom_css = (process.env.CUSTOM_CSS ||'').split(/ +/).filter(Boolean) 33 | 34 | const p = fn => (req, res, next) => fn(req, res).catch(next) 35 | 36 | app.get('/', (req, res) => res.render(rpath('client/index.pug'))) 37 | app.get('/app.js', browserify(rpath('client/src/run-browser.js'))) 38 | 39 | // Merges the main stylesheet from www/style.css with the custom css files 40 | app.get('/style.css', p(async (req, res) => 41 | res.type('css').send(await prepCss()))) 42 | 43 | const prepCss = async _ => 44 | (await Promise.all([ rpath('www/style.css'), ...custom_css ].map(path => fsp.readFile(path)))) 45 | .join('\n') 46 | 47 | // Automatically adjust CSS for RTL using cssjanus 48 | app.get('/style-rtl.css', p(async (req, res) => 49 | res.type('css').send(cssjanus.transform(await prepCss())))) 50 | 51 | // Add handlers for custom asset overrides 52 | custom_assets.forEach(pattern => { 53 | // pattern could also be a simple path 54 | const paths = glob.sync(pattern) 55 | 56 | paths.forEach(path => { 57 | const name = pathu.basename(path) 58 | , stat = fs.statSync(path) 59 | 60 | stat.isDirectory() 61 | ? app.use('/'+name, express.static(path)) 62 | : app.get('/'+name, (req, res) => res.sendFile(path)) 63 | }) 64 | }) 65 | 66 | // And finally the default fallback assets from www/ 67 | app.use('/', express.static(rpath('www'))) 68 | 69 | app.use((req, res) => res.render(rpath('client/index.pug'))) 70 | 71 | app.listen(process.env.PORT || 5000, function(){ 72 | console.log(`HTTP server running on ${this.address().address}:${this.address().port}`) 73 | }) 74 | -------------------------------------------------------------------------------- /flavors/bitcoin-mainnet/config.env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export SITE_TITLE='Bitcoin Explorer' 4 | export HOME_TITLE='Bitcoin Explorer' 5 | export NATIVE_ASSET_LABEL=BTC 6 | export NATIVE_ASSET_NAME=Bitcoin 7 | export MENU_ACTIVE='Bitcoin' 8 | -------------------------------------------------------------------------------- /flavors/bitcoin-regtest/config.env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export SITE_TITLE='Bitcoin Regtest Explorer' 4 | export HOME_TITLE='Bitcoin Regtest Explorer' 5 | export NATIVE_ASSET_LABEL=rBTC 6 | export NATIVE_ASSET_NAME='Bitcoin Regtest' 7 | 8 | export MENU_ACTIVE='Bitcoin Regtest' 9 | export BASE_HREF=${BASE_HREF:-'/regtest/'} 10 | 11 | export CUSTOM_ASSETS="$CUSTOM_ASSETS flavors/bitcoin-regtest/www/*" 12 | export CUSTOM_CSS="$CUSTOM_CSS flavors/bitcoin-testnet/extras.css" 13 | 14 | -------------------------------------------------------------------------------- /flavors/bitcoin-regtest/www/img/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/bitcoin-regtest/www/img/block.png -------------------------------------------------------------------------------- /flavors/bitcoin-regtest/www/img/icons/menu-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/bitcoin-regtest/www/img/icons/menu-logo.png -------------------------------------------------------------------------------- /flavors/bitcoin-regtest/www/img/icons/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/bitcoin-regtest/www/img/icons/minus.png -------------------------------------------------------------------------------- /flavors/bitcoin-regtest/www/img/icons/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/bitcoin-regtest/www/img/icons/plus.png -------------------------------------------------------------------------------- /flavors/bitcoin-regtest/www/img/icons/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/bitcoin-regtest/www/img/icons/search.png -------------------------------------------------------------------------------- /flavors/bitcoin-regtest/www/img/transaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/bitcoin-regtest/www/img/transaction.png -------------------------------------------------------------------------------- /flavors/bitcoin-signet/config.env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export SITE_TITLE='Bitcoin Signet Explorer' 4 | export HOME_TITLE='Bitcoin Signet Explorer' 5 | export NATIVE_ASSET_LABEL=sBTC 6 | export NATIVE_ASSET_NAME='Bitcoin Signet' 7 | 8 | export MENU_ACTIVE='Bitcoin Signet' 9 | export BASE_HREF=${BASE_HREF:-'/signet/'} 10 | 11 | export CUSTOM_ASSETS="$CUSTOM_ASSETS flavors/bitcoin-signet/www/*" 12 | export CUSTOM_CSS="$CUSTOM_CSS flavors/bitcoin-testnet/extras.css" 13 | 14 | -------------------------------------------------------------------------------- /flavors/bitcoin-signet/www/img/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/bitcoin-signet/www/img/block.png -------------------------------------------------------------------------------- /flavors/bitcoin-signet/www/img/icons/menu-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/bitcoin-signet/www/img/icons/menu-logo.png -------------------------------------------------------------------------------- /flavors/bitcoin-signet/www/img/icons/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/bitcoin-signet/www/img/icons/minus.png -------------------------------------------------------------------------------- /flavors/bitcoin-signet/www/img/icons/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/bitcoin-signet/www/img/icons/plus.png -------------------------------------------------------------------------------- /flavors/bitcoin-signet/www/img/icons/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/bitcoin-signet/www/img/icons/search.png -------------------------------------------------------------------------------- /flavors/bitcoin-signet/www/img/transaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/bitcoin-signet/www/img/transaction.png -------------------------------------------------------------------------------- /flavors/bitcoin-testnet/config.env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export SITE_TITLE='Bitcoin Testnet Explorer' 4 | export HOME_TITLE='Bitcoin Testnet Explorer' 5 | export NATIVE_ASSET_LABEL=tBTC 6 | export NATIVE_ASSET_NAME='Bitcoin Testnet' 7 | 8 | export MENU_ACTIVE='Bitcoin Testnet' 9 | export BASE_HREF=${BASE_HREF:-'/testnet/'} 10 | 11 | export CUSTOM_ASSETS="$CUSTOM_ASSETS flavors/bitcoin-testnet/www/*" 12 | export CUSTOM_CSS="$CUSTOM_CSS flavors/bitcoin-testnet/extras.css" 13 | -------------------------------------------------------------------------------- /flavors/bitcoin-testnet/extras.css: -------------------------------------------------------------------------------- 1 | #BitcoinTestnet, #LiquidTestnet{ 2 | display: block; 3 | } 4 | 5 | #Bitcoin, #Liquid{ 6 | display: none; 7 | } 8 | 9 | .main-nav li.active a{ 10 | border: 1px solid rgba(168, 184, 201, 1); 11 | } 12 | 13 | .sub-nav .active { 14 | border-bottom: solid 2px rgba(168, 184, 201, 1); 15 | } 16 | 17 | .details-btn > div { 18 | color: rgba(168, 184, 201, 1); 19 | border: 1px solid rgba(168, 184, 201, 1); 20 | } 21 | 22 | .transaction-box > .footer > div:nth-child(3) { 23 | color: rgba(168, 184, 201, 1); 24 | } 25 | 26 | .navbar { 27 | background-image: linear-gradient(-90deg, rgba(84, 103, 124, 1) 0%, rgba(29, 72, 111, 1) 18%, rgba(24, 53, 80, 1) 36%, rgba(29, 37, 48, 1) 58%, rgba(14, 16, 17, 1) 100%); 28 | } 29 | 30 | .sub-nav a sup.highlight{ 31 | display: none; 32 | } 33 | 34 | .main-nav li a{ 35 | color: #78838e; 36 | } 37 | 38 | .nav-link:hover{ 39 | color: white !important; 40 | } 41 | 42 | .sub-navbar:before { 43 | content: "Bitcoin Testnet is used for testing. Funds have no value!"; 44 | width: 100%; 45 | height: 35px; 46 | background: #ba042a; 47 | position: absolute; 48 | margin-top: -10px; 49 | text-align: center; 50 | font-size: 14px; 51 | line-height: 2.4; 52 | } 53 | 54 | .sub-nav-container{ 55 | margin-top: 25px; 56 | } 57 | 58 | .table-title, .block-header-title, .transaction-header-title{ 59 | display: flex; 60 | } 61 | 62 | .table-title:after, .block-header-title:after, .transaction-header-title:after { 63 | content: "Testnet"; 64 | font-size: 13px; 65 | background-color: rgb(48 59 70); 66 | padding: 3px 12px; 67 | border-radius: 30px; 68 | margin-left: 15px; 69 | display: inline-block; 70 | align-self: center; 71 | } -------------------------------------------------------------------------------- /flavors/bitcoin-testnet/www/img/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/bitcoin-testnet/www/img/block.png -------------------------------------------------------------------------------- /flavors/bitcoin-testnet/www/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/bitcoin-testnet/www/img/favicon.png -------------------------------------------------------------------------------- /flavors/bitcoin-testnet/www/img/icons/menu-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | btc_test 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /flavors/bitcoin-testnet/www/img/icons/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/bitcoin-testnet/www/img/icons/minus.png -------------------------------------------------------------------------------- /flavors/bitcoin-testnet/www/img/icons/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/bitcoin-testnet/www/img/icons/plus.png -------------------------------------------------------------------------------- /flavors/bitcoin-testnet/www/img/icons/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/bitcoin-testnet/www/img/icons/search.png -------------------------------------------------------------------------------- /flavors/bitcoin-testnet/www/img/transaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/bitcoin-testnet/www/img/transaction.png -------------------------------------------------------------------------------- /flavors/blockstream/LICENSE.md: -------------------------------------------------------------------------------- 1 | The Blockstream logos contained in this repository and the brand name "Blockstream" 2 | (collectively "Blockstream logos") are not part of the MIT license. 3 | Please contact Blockstream at inquiries@blockstream.com to inquiry about permitted use. 4 | -------------------------------------------------------------------------------- /flavors/blockstream/config.env: -------------------------------------------------------------------------------- 1 | export SITE_DESC='Blockstream Explorer is an open source block explorer providing detailed blockchain data across Bitcoin, Testnet, and Liquid. Supports Tor and tracking-free.' 2 | export SITE_FOOTER='© 2025 Blockstream Corporation Inc. All rights reserved.' 3 | export HEAD_HTML=\ 4 | ''\ 5 | ''\ 6 | ''\ 7 | ''\ 8 | ''\ 9 | ''\ 10 | ''\ 11 | ''\ 12 | '' 13 | 14 | # Base URL for opensearch and canonical tag 15 | # Should always point to the js-enabled website 16 | YESJS_BASE_HREF="${BASE_HREF//\/nojs\//\/}" 17 | export CANONICAL_URL="https://blockstream.info${YESJS_BASE_HREF:-/}" 18 | 19 | export MENU_ITEMS='{ 20 | "Bitcoin": "/" 21 | , "Liquid": "/liquid/" 22 | , "Bitcoin Testnet": "/testnet/" 23 | , "Liquid Testnet": "/liquidtestnet/" 24 | }' 25 | 26 | export FOOTER_LINKS='{ 27 | "/img/x-white.svg": "https://x.com/Blockstream" 28 | , "/img/linkedin_blue.png": "https://ca.linkedin.com/company/blockstream" 29 | , "/img/f1b_blue.png": "https://www.facebook.com/Blockstream/" 30 | , "/img/github_blue.png": "https://github.com/Blockstream/esplora" 31 | }' 32 | 33 | export SITE_TITLE="$SITE_TITLE - Blockstream.info" 34 | 35 | export ONION_V3="http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion" 36 | 37 | export CUSTOM_ASSETS="$CUSTOM_ASSETS flavors/blockstream/www/*" 38 | export CUSTOM_CSS="$CUSTOM_CSS flavors/blockstream/extras.css" 39 | 40 | export NOSCRIPT_REDIR=1 41 | 42 | export NAVBAR_HTML=1 43 | 44 | export TERMS="https://blockstream.com/terms/" 45 | export PRIVACY="https://blockstream.com/privacy/" 46 | -------------------------------------------------------------------------------- /flavors/blockstream/electrum-banner.txt: -------------------------------------------------------------------------------- 1 | Welcome to electrum.blockstream.info - no logs and Tor support - Please learn about running your own server! 2 | -------------------------------------------------------------------------------- /flavors/blockstream/electrum-hosts-bitcoin-mainnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "electrum.blockstream.info": { "tcp_port": 50001, "ssl_port": 50002 }, 3 | "explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion": { "tcp_port": 110 } 4 | } 5 | -------------------------------------------------------------------------------- /flavors/blockstream/electrum-hosts-bitcoin-testnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "electrum.blockstream.info": { "tcp_port": 60001, "ssl_port": 60002 }, 3 | "explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion": { "tcp_port": 143 } 4 | } 5 | -------------------------------------------------------------------------------- /flavors/blockstream/electrum-hosts-liquid-mainnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "electrum.blockstream.info": { "tcp_port": 50401, "ssl_port": 50402 }, 3 | "explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion": { "tcp_port": 195 } 4 | } 5 | -------------------------------------------------------------------------------- /flavors/blockstream/extras.css: -------------------------------------------------------------------------------- 1 | .navbar-brand::before { 2 | background: url(img/icons/explorer_logo.svg); 3 | background-size: contain; 4 | width: 245px; 5 | margin: 0; 6 | background-repeat: no-repeat; 7 | } 8 | 9 | .theme-light .navbar-brand::before { 10 | background: url(img/icons/explorer_dark_logo.svg); 11 | background-repeat: no-repeat; 12 | background-size: contain; 13 | width: 245px; 14 | margin: 0; 15 | } 16 | 17 | .footer-logo::before { 18 | width: 220px; 19 | height: 93px; 20 | background-image: url(img/blockstream-full-logo.png); 21 | background-size: 100%; 22 | content: ' '; 23 | display: block; 24 | } 25 | .footer-logo { 26 | margin-right: 0px; 27 | } 28 | .footer-links { 29 | margin-top: 5px; 30 | } 31 | .theme-light .footer-logo::before { 32 | background-image: url(img/blockstream-full-logo-light.png); 33 | } 34 | 35 | @media only screen and (max-width: 1000px) { 36 | .footer-logo::before { 37 | width: 200px; 38 | height: 83px; 39 | } 40 | } 41 | 42 | @media only screen and (max-width: 900px) { 43 | .footer-logo::before { 44 | margin-top: 30px; 45 | } 46 | 47 | .theme-light .navbar-brand::before, .navbar-brand::before { 48 | 49 | background-size: 54px 54px; 50 | width: 54px; 51 | } 52 | 53 | .theme-light .navbar-brand::before { 54 | background: url(img/icons/dark-logo-icon.svg); 55 | background-repeat: no-repeat; 56 | } 57 | 58 | .navbar-brand::before { 59 | background: url(img/icons/light-logo-icon.svg); 60 | background-repeat: no-repeat; 61 | } 62 | } 63 | 64 | .theme-light .primary-btn, 65 | .theme-light .primary-btn:link, 66 | .theme-light .primary-btn:visited, 67 | .theme-light .primary-btn:hover, 68 | .theme-light .primary-btn:focus { 69 | color: #0C0C0F; 70 | } 71 | 72 | .theme-light .hero-section p, .theme-light .info-section p, .theme-light .feature p, .theme-light .cta-section p, .theme-light .pricing-card p { 73 | color: #080B0E; 74 | } 75 | 76 | .theme-light .cta-section { 77 | background-color: #F6F7F9; 78 | } 79 | 80 | .theme-light .pricing-card { 81 | background: #fff; 82 | } 83 | 84 | .theme-light .pricing-card p > a { 85 | color: #FF9417; 86 | } -------------------------------------------------------------------------------- /flavors/blockstream/www/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/blockstream/www/favicon.ico -------------------------------------------------------------------------------- /flavors/blockstream/www/img/blockstream-full-logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/blockstream/www/img/blockstream-full-logo-light.png -------------------------------------------------------------------------------- /flavors/blockstream/www/img/blockstream-full-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/blockstream/www/img/blockstream-full-logo.png -------------------------------------------------------------------------------- /flavors/blockstream/www/img/f1b_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/blockstream/www/img/f1b_blue.png -------------------------------------------------------------------------------- /flavors/blockstream/www/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/blockstream/www/img/favicon.png -------------------------------------------------------------------------------- /flavors/blockstream/www/img/icons/blockstream-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/blockstream/www/img/icons/blockstream-logo.png -------------------------------------------------------------------------------- /flavors/blockstream/www/img/icons/dark-logo-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /flavors/blockstream/www/img/icons/light-logo-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /flavors/blockstream/www/img/linkedin_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/blockstream/www/img/linkedin_blue.png -------------------------------------------------------------------------------- /flavors/blockstream/www/img/social-sharing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/blockstream/www/img/social-sharing.png -------------------------------------------------------------------------------- /flavors/blockstream/www/img/x-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /flavors/liquid-mainnet/config.env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export SITE_TITLE='Liquid Explorer' 4 | export HOME_TITLE='Liquid Explorer' 5 | export NATIVE_ASSET_ID="6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d" 6 | export NATIVE_ASSET_LABEL=L-BTC 7 | export NATIVE_ASSET_NAME='Liquid Bitcoin' 8 | export IS_ELEMENTS=1 9 | 10 | export ASSET_MAP_URL=./_data/assets.minimal.json 11 | 12 | export MENU_ACTIVE='Liquid' 13 | export BASE_HREF=${BASE_HREF:-'/liquid/'} 14 | 15 | export CUSTOM_ASSETS="$CUSTOM_ASSETS flavors/liquid-mainnet/www/*" 16 | export CUSTOM_CSS="$CUSTOM_CSS flavors/liquid/extras.css flavors/liquid-mainnet/extras.css" 17 | -------------------------------------------------------------------------------- /flavors/liquid-mainnet/extras.css: -------------------------------------------------------------------------------- 1 | 2 | .sub-nav .active{ 3 | border-bottom: solid 2px #46beae; 4 | } 5 | 6 | .details-btn > div { 7 | color: #46beae; 8 | border: 1px solid #46beae; 9 | background: none; 10 | } 11 | 12 | .transaction-box > .footer > div:nth-child(3) { 13 | color: #46beae; 14 | } 15 | 16 | .navbar { 17 | background-image: linear-gradient(-90deg, rgba(13, 141, 119, 1) 0%, rgba(17, 103, 97, 1) 16%, rgba(25, 68, 74, 1) 35%, rgba(29, 42, 48, 1) 57%, rgba(14, 16, 17, 1) 100%); 18 | } 19 | 20 | .main-nav li.active a{ 21 | border: 1px solid #46beae; 22 | } -------------------------------------------------------------------------------- /flavors/liquid-mainnet/www/img/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/liquid-mainnet/www/img/block.png -------------------------------------------------------------------------------- /flavors/liquid-mainnet/www/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/liquid-mainnet/www/img/favicon.png -------------------------------------------------------------------------------- /flavors/liquid-mainnet/www/img/icons/menu-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | lbtc_main 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /flavors/liquid-mainnet/www/img/icons/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/liquid-mainnet/www/img/icons/minus.png -------------------------------------------------------------------------------- /flavors/liquid-mainnet/www/img/icons/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/liquid-mainnet/www/img/icons/plus.png -------------------------------------------------------------------------------- /flavors/liquid-mainnet/www/img/icons/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/liquid-mainnet/www/img/icons/search.png -------------------------------------------------------------------------------- /flavors/liquid-mainnet/www/img/transaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/liquid-mainnet/www/img/transaction.png -------------------------------------------------------------------------------- /flavors/liquid-regtest/config.env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export SITE_TITLE='Liquid Regtest Explorer' 4 | export HOME_TITLE='Liquid Regtest Explorer' 5 | export NATIVE_ASSET_ID=5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225 6 | export NATIVE_ASSET_LABEL=rL-BTC 7 | export NATIVE_ASSET_NAME="Liquid Regtest Bitcoin" 8 | 9 | export IS_ELEMENTS=1 10 | 11 | export MENU_ACTIVE='Liquid Regtest' 12 | export BASE_HREF=${BASE_HREF:-'/liquidregtest/'} 13 | 14 | export CUSTOM_ASSETS="$CUSTOM_ASSETS flavors/liquid-regtest/www/*" 15 | export CUSTOM_CSS="$CUSTOM_CSS flavors/liquid/extras.css flavors/bitcoin-testnet/extras.css" 16 | 17 | -------------------------------------------------------------------------------- /flavors/liquid-regtest/www/img/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/liquid-regtest/www/img/block.png -------------------------------------------------------------------------------- /flavors/liquid-regtest/www/img/icons/menu-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | btc_test 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /flavors/liquid-regtest/www/img/icons/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/liquid-regtest/www/img/icons/minus.png -------------------------------------------------------------------------------- /flavors/liquid-regtest/www/img/icons/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/liquid-regtest/www/img/icons/plus.png -------------------------------------------------------------------------------- /flavors/liquid-regtest/www/img/icons/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/liquid-regtest/www/img/icons/search.png -------------------------------------------------------------------------------- /flavors/liquid-regtest/www/img/transaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/liquid-regtest/www/img/transaction.png -------------------------------------------------------------------------------- /flavors/liquid-testnet/config.env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export SITE_TITLE='Liquid Testnet Explorer' 4 | export HOME_TITLE='Liquid Testnet Explorer' 5 | export NATIVE_ASSET_ID=144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49 6 | export NATIVE_ASSET_LABEL=tL-BTC 7 | export NATIVE_ASSET_NAME="Liquid Testnet Bitcoin" 8 | 9 | export IS_ELEMENTS=1 10 | export ASSET_MAP_URL=./_data/assets.minimal.json 11 | 12 | export MENU_ACTIVE='Liquid Testnet' 13 | export BASE_HREF=${BASE_HREF:-'/liquidtestnet/'} 14 | 15 | export CUSTOM_ASSETS="$CUSTOM_ASSETS flavors/liquid-testnet/www/*" 16 | export CUSTOM_CSS="$CUSTOM_CSS flavors/liquid/extras.css flavors/liquid-testnet/extras.css" 17 | -------------------------------------------------------------------------------- /flavors/liquid-testnet/extras.css: -------------------------------------------------------------------------------- 1 | 2 | #BitcoinTestnet, #LiquidTestnet{ 3 | display: block; 4 | } 5 | 6 | #Bitcoin, #Liquid{ 7 | display: none; 8 | } 9 | 10 | .main-nav li.active a{ 11 | border: 1px solid rgba(168, 184, 201, 1); 12 | } 13 | 14 | .sub-nav .active { 15 | border-bottom: solid 2px rgba(168, 184, 201, 1); 16 | } 17 | 18 | .details-btn > div { 19 | color: rgba(168, 184, 201, 1); 20 | border: 1px solid rgba(168, 184, 201, 1); 21 | } 22 | 23 | .transaction-box > .footer > div:nth-child(3) { 24 | color: rgba(168, 184, 201, 1); 25 | } 26 | 27 | .navbar { 28 | background-image: linear-gradient(-90deg, rgba(84, 103, 124, 1) 0%, rgba(29, 72, 111, 1) 18%, rgba(24, 53, 80, 1) 36%, rgba(29, 37, 48, 1) 58%, rgba(14, 16, 17, 1) 100%); 29 | } 30 | 31 | .sub-nav a sup.highlight{ 32 | display: none; 33 | } 34 | 35 | .table-title, .block-header-title, .transaction-header-title, .asset-page h1 { 36 | display: flex; 37 | } 38 | 39 | .main-nav li a{ 40 | color: #78838e; 41 | } 42 | 43 | .nav-link:hover{ 44 | color: white !important; 45 | } 46 | 47 | .sub-navbar:before { 48 | content: "Liquid Testnet is used for testing. Funds have no value!"; 49 | width: 100%; 50 | height: 35px; 51 | background: #ba042a; 52 | position: absolute; 53 | margin-top: -10px; 54 | text-align: center; 55 | font-size: 14px; 56 | line-height: 2.4; 57 | } 58 | 59 | .sub-nav-container{ 60 | margin-top: 25px; 61 | } 62 | 63 | .table-title:after, .block-header-title:after, .transaction-header-title:after, .asset-page h1:after { 64 | content: "Testnet"; 65 | font-size: 13px; 66 | background-color: rgb(48 59 70); 67 | padding: 3px 12px; 68 | border-radius: 30px; 69 | margin-left: 15px; 70 | display: inline-block; 71 | align-self: center; 72 | } 73 | 74 | -------------------------------------------------------------------------------- /flavors/liquid-testnet/www/img/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/liquid-testnet/www/img/block.png -------------------------------------------------------------------------------- /flavors/liquid-testnet/www/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/liquid-testnet/www/img/favicon.png -------------------------------------------------------------------------------- /flavors/liquid-testnet/www/img/icons/menu-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | lbtc_test 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /flavors/liquid-testnet/www/img/icons/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/liquid-testnet/www/img/icons/minus.png -------------------------------------------------------------------------------- /flavors/liquid-testnet/www/img/icons/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/liquid-testnet/www/img/icons/plus.png -------------------------------------------------------------------------------- /flavors/liquid-testnet/www/img/icons/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/liquid-testnet/www/img/icons/search.png -------------------------------------------------------------------------------- /flavors/liquid-testnet/www/img/transaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/flavors/liquid-testnet/www/img/transaction.png -------------------------------------------------------------------------------- /lang/README.md: -------------------------------------------------------------------------------- 1 | The `.json` files in this directory are auto-generated from the `.po` files 2 | using the `build-json.sh` utility script. Please do not edit `.json` files 3 | directly. 4 | 5 | To contribute translations, please visit https://www.transifex.com/blockstream/esplora 6 | -------------------------------------------------------------------------------- /lang/bg.json: -------------------------------------------------------------------------------- 1 | { 2 | "Address": "Адрес", 3 | "Address: %s": "Адрес: %0", 4 | "Block %s": "Блок %0", 5 | "Block #%s: %s": "Блок #%0: %1", 6 | "Details": "Детайли", 7 | "Height": "Височина", 8 | "In best chain (%s confirmations)": [ 9 | "Най-дългата верига (1 потвърждение)", 10 | "Най-дългата верига (%0 потвърждения)" 11 | ], 12 | "Included in Block": "Включена в блок", 13 | "lang_id": "bg", 14 | "lang_name": "Български", 15 | "Loading...": "Зарежда се…", 16 | "Load more": "Още резултати", 17 | "Next": "Следващ", 18 | "Nonstandard": "Нестандартна", 19 | "No results found": "Няма намерени резултати", 20 | "OP_RETURN data": "OP_RETURN данни", 21 | "Output in parent chain": "Дериват от веригата източник", 22 | "Page Not Found": "Страницата не е намерена", 23 | "Peg-out": "Peg-оut", 24 | "Peg-out address": "Peg-out адрес", 25 | "Peg-out to": "Peg-out до", 26 | "Previous": "Предишен", 27 | "Search for block height, hash, transaction, or address": "Въведете височина на блок, хаш, транзакция или адрес", 28 | "Size (KB)": "Размер (KB)", 29 | "%s of %s Transactions": "%0 от %1 Транзакции", 30 | "Spent by": "Похарчен от", 31 | "Status": "Статус", 32 | "%s Transactions": [ 33 | "1 Транзакция", 34 | "%0 Транзакции" 35 | ], 36 | "Timestamp": "Времeви печат", 37 | "Transaction": "Транзакция", 38 | "Transaction fees": "Такси за транзакция", 39 | "Transactions": "Транзакции", 40 | "Transaction: %s": "Транзакция: %0", 41 | "Type": "Тип", 42 | "Unspent": "Не e похарчен", 43 | "Version": "Версия", 44 | "We encountered an error. Please try again later.": "Изникна грешка. Моля, опитайте по-късно.", 45 | "Weight (KWU)": "Тегло (KWU)", 46 | "Witness": "Подпис" 47 | } 48 | -------------------------------------------------------------------------------- /lang/bs.json: -------------------------------------------------------------------------------- 1 | { 2 | "Address": "Adresa", 3 | "Address: %s": "Adresa: %0", 4 | "Block %s": "blok %0", 5 | "Block #%s: %s": "Blok #%0: %1", 6 | "Confidential": "Poverljivo", 7 | "Details": "Detalji", 8 | "Height": "Visina", 9 | "In best chain (%s confirmations)": [ 10 | "U najnoljem lancu (1 conferma)", 11 | "U najnoljem lancu (%0 conferme)" 12 | ], 13 | "Included in Block": "Ukljuceno u blok", 14 | "lang_id": "bs", 15 | "lang_name": "Bosanski", 16 | "Loading...": "Ucitavanje...", 17 | "Load more": "Ucitaj jos", 18 | "Next": "Sledece", 19 | "No results found": "Nema rezultata", 20 | "Page Not Found": "Strana nije pronadjena", 21 | "Previous": "Prethodni", 22 | "%s Confirmations": [ 23 | "1 Potvrda", 24 | "%0 Potvrda" 25 | ], 26 | "Search for block height, hash, transaction, or address": "Pretraga po visini bloka, hash, transakciji ili adresi", 27 | "Size (KB)": "Velicina (KB)", 28 | "%s of %s Transactions": "%0 od %1 Transakicja", 29 | "Spent by": "Potroseni od strane", 30 | "%s Transactions": [ 31 | "%0 Transakcija", 32 | "%0 Transakcija" 33 | ], 34 | "Timestamp": "Vreme", 35 | "Transaction": "Transakcija", 36 | "Transactions": "Transakcije", 37 | "Transaction: %s": "Transakcija: %0", 38 | "Type": "Tip", 39 | "Unconfirmed": "Nema Potvrda", 40 | "Unspent": "Nepotroseni", 41 | "Version": "Verzija", 42 | "Weight (KWU)": "Tezina (KWU)" 43 | } 44 | -------------------------------------------------------------------------------- /lang/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "Address": "Adresse", 3 | "Address: %s": "Adresse: %0", 4 | "Block %s": "Block %0", 5 | "Block #%s: %s": "Block #%0: %1", 6 | "Height": "Höhe", 7 | "In best chain (%s confirmations)": [ 8 | "In bester Chain (1 Bestätigung)", 9 | "In bester Chain (%0 Bestätigungen)" 10 | ], 11 | "Included in Block": "Beinhaltet in Block", 12 | "lang_id": "de", 13 | "lang_name": "Deutsch", 14 | "Loading...": "Wird geladen...", 15 | "Load more": "Mehr laden", 16 | "Next": "Nächster", 17 | "No results found": "Keine Ergebnisse gefunden", 18 | "Page Not Found": "Seite nicht gefunden", 19 | "Previous": "Vorheriger", 20 | "%s Confirmations": [ 21 | "1 Bestätigung", 22 | "%0 Bestätigungen" 23 | ], 24 | "Search for block height, hash, transaction, or address": "Suche nach Blockhöhe, Hash, Transaktion oder Adresse", 25 | "Size (KB)": "Grösse (KB)", 26 | "%s of %s Transactions": "%0 von %1 Transaktionen", 27 | "Spent by": "Ausgegeben von", 28 | "%s Transactions": [ 29 | "%0 Transaktionen", 30 | "%0 Transaktionen" 31 | ], 32 | "Timestamp": "Zeitstempel", 33 | "Transaction": "Transaktion", 34 | "Transaction fees": "Gebühr", 35 | "Transactions": "Transaktionen", 36 | "Transaction: %s": "Transaktion: %0", 37 | "Unconfirmed": "Unbestätigt", 38 | "Unspent": "Nicht ausgegeben", 39 | "Weight (KWU)": "Gewicht (KWU)" 40 | } 41 | -------------------------------------------------------------------------------- /lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "In best chain (%s confirmations)": [ 3 | "In best chain (1 confirmation)", 4 | "In best chain (%0 confirmations)" 5 | ], 6 | "lang_id": "en", 7 | "lang_name": "English", 8 | "%s Confirmations": [ 9 | "1 Confirmation", 10 | "%0 Confirmations" 11 | ], 12 | "%s outputs": [ 13 | "1 output", 14 | "%0 outputs" 15 | ], 16 | "%s Transactions": [ 17 | "1 Transaction", 18 | "%0 Transactions" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /lang/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "Address": "Dirección", 3 | "Address: %s": "Dirección %0", 4 | "Block %s": "Bloque %0", 5 | "Block #%s: %s": "Bloque #%0: %1", 6 | "Details": "Detalles", 7 | "Height": "Altura", 8 | "In best chain (%s confirmations)": [ 9 | "En cadena más larga (%0 confirmaciones)", 10 | "En cadena más larga (%0 confirmaciones)" 11 | ], 12 | "Included in Block": "Incluida en bloque", 13 | "lang_id": "es", 14 | "lang_name": "Español", 15 | "Loading...": "Cargando...", 16 | "Load more": "Mostrar siguiente", 17 | "Next": "Siguiente", 18 | "Nonstandard": "No estándar", 19 | "No results found": "No se encontraron resultados para su búsqueda", 20 | "OP_RETURN data": "Datos OP_RETURN", 21 | "Output in parent chain": "Salida en cadena principal", 22 | "Page Not Found": "Página No Encontrada", 23 | "Peg-out": "Peg-Out", 24 | "Peg-out address": "Dirección de Peg-out ", 25 | "Peg-out to": "Peg-out a", 26 | "Powered by esplora": "Desarrollado por esplora", 27 | "Previous": "Previo", 28 | "Search for block height, hash, transaction, or address": "Busca por altura de bloque, hash, transacción o dirección", 29 | "Size (KB)": "Tamaño (KB)", 30 | "%s of %s Transactions": "Transacciones %0 de %1", 31 | "Spent by": "Gastado por", 32 | "Status": "Estatus", 33 | "%s Transactions": [ 34 | "Transacciones %0", 35 | "Transacciones %0" 36 | ], 37 | "Timestamp": "Fecha y Hora", 38 | "Transaction": "Transacción", 39 | "Transaction fees": "Tarifa for transacción", 40 | "Transactions": "Transacciones", 41 | "Transaction: %s": "Transacción: %0", 42 | "Type": "Tipo", 43 | "Unspent": "No gastado", 44 | "Version": "Versión", 45 | "We encountered an error. Please try again later.": "Encontramos un error. Por favor intenta nuevamente más tarde.", 46 | "Weight (KWU)": "Peso (KWU)", 47 | "Witness": "testigo" 48 | } 49 | -------------------------------------------------------------------------------- /lang/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Address": "Adresse", 3 | "Address: %s": "Adresse: %0", 4 | "Block %s": "Bloc %0", 5 | "Block #%s: %s": "Bloc #%0: %1", 6 | "Confidential": "Confidentiel", 7 | "Details": "Détails", 8 | "Height": "Hauteur", 9 | "In best chain (%s confirmations)": [ 10 | "Dans la chaine la plus longue (1 confirmation)", 11 | "Dans la chaine la plus longue (%0 confirmations)" 12 | ], 13 | "Included in Block": "Inclue dans le bloc", 14 | "lang_id": "fr", 15 | "lang_name": "Français", 16 | "Loading...": "Chargement...", 17 | "Load more": "Afficher plus", 18 | "Next": "Suivant", 19 | "No results found": "Aucun résultat", 20 | "Page Not Found": "Page non trouvée", 21 | "Previous": "Précédent", 22 | "%s Confirmations": [ 23 | "1 confirmation", 24 | "%0 confirmations" 25 | ], 26 | "Search for block height, hash, transaction, or address": "Numéro de bloc, hash, transaction ou adresse", 27 | "Size (KB)": "Taille (Ko)", 28 | "%s of %s Transactions": "%0 sur %1 Transactions", 29 | "Spent by": "Dépensé par", 30 | "Status": "Statut", 31 | "%s Transactions": [ 32 | "%0 Transactions", 33 | "%0 Transactions" 34 | ], 35 | "Timestamp": "Date", 36 | "Transaction fees": "Frais de transaction", 37 | "Transaction: %s": "Transaction: %0", 38 | "Unconfirmed": "Non confirmée", 39 | "Unspent": "Non dépensé", 40 | "Weight (KWU)": "Poids (KWU)" 41 | } 42 | -------------------------------------------------------------------------------- /lang/hr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Address": "Adresa", 3 | "Address: %s": "Adresa: %0", 4 | "Block %s": "blok %0", 5 | "Block #%s: %s": "Blok #%0: %1", 6 | "Confidential": "Poverljivo", 7 | "Details": "Detalji", 8 | "Height": "Visina", 9 | "In best chain (%s confirmations)": [ 10 | "U najnoljem lancu (1 conferma)", 11 | "U najnoljem lancu (%0 conferme)" 12 | ], 13 | "Included in Block": "Ukljuceno u blok", 14 | "lang_id": "hr", 15 | "lang_name": "Hrvatski", 16 | "Loading...": "Ucitavanje...", 17 | "Load more": "Ucitaj jos", 18 | "Next": "Sledece", 19 | "No results found": "Nema rezultata", 20 | "Page Not Found": "Strana nije pronadjena", 21 | "Previous": "Prethodni", 22 | "%s Confirmations": [ 23 | "1 Potvrda", 24 | "%0 Potvrda" 25 | ], 26 | "Search for block height, hash, transaction, or address": "Pretraga po visini bloka, hash, transakciji ili adresi", 27 | "Size (KB)": "Velicina (KB)", 28 | "%s of %s Transactions": "%0 od %1 Transakicja", 29 | "Spent by": "Potroseni od strane", 30 | "%s Transactions": [ 31 | "%0 Transakcija", 32 | "%0 Transakcija" 33 | ], 34 | "Timestamp": "Vreme", 35 | "Transaction": "Transakcija", 36 | "Transactions": "Transakcije", 37 | "Transaction: %s": "Transakcija: %0", 38 | "Type": "Tip", 39 | "Unconfirmed": "Nema Potvrda", 40 | "Unspent": "Nepotroseni", 41 | "Version": "Verzija", 42 | "Weight (KWU)": "Tezina (KWU)" 43 | } 44 | -------------------------------------------------------------------------------- /lang/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | en: require('./en.json') 3 | , 'pt-pt': require('./pt-pt.json') 4 | , de: require('./de.json') 5 | , fr: require('./fr.json') 6 | , it: require('./it.json') 7 | , es: require('./es.json') 8 | , nl: require('./nl.json') 9 | , bg: require('./bg.json') 10 | , ru: require('./ru.json') 11 | , sr: require('./sr.json') 12 | , hr: require('./hr.json') 13 | , bs: require('./bs.json') 14 | , me: require('./me.json') 15 | , sv: require('./sv.json') 16 | , 'zh-cn': require('./zh-cn.json') 17 | , he: require('./he.json') 18 | , jp: require('./jp.json') 19 | , ko: require('./ko.json') 20 | } 21 | -------------------------------------------------------------------------------- /lang/jp.json: -------------------------------------------------------------------------------- 1 | { 2 | "Address": "アドレス", 3 | "Address: %s": "アドレス: %0", 4 | "Block %s": "ブロック %0", 5 | "Block #%s: %s": "ブロック #%0: %1", 6 | "Coinbase": "コインベース", 7 | "Details": "詳細", 8 | "Height": "ブロック高", 9 | "In best chain (%s confirmations)": [ 10 | "最長チェーン(%0 検証)", 11 | "最長チェーン(%0 検証)" 12 | ], 13 | "Included in Block": "ブロック内容", 14 | "lang_id": "jp", 15 | "lang_name": "日本語", 16 | "Loading...": "ローディング中", 17 | "Load more": "さらに表示", 18 | "Lock time": "ロックタイム", 19 | "Next": "次へ", 20 | "Nonstandard": "ノン・スタンダード", 21 | "No results found": "結果がありません", 22 | "OP_RETURN data": "OP_RETURN データ", 23 | "Output in parent chain": "親チェーンのアウトプット", 24 | "Page Not Found": "ページが見つかりません。", 25 | "Peg-out address": "Peg-out アドレス", 26 | "Peg-out to": "Peg-out先", 27 | "Previous": "前に", 28 | "Search for block height, hash, transaction, or address": "ブロック高、ハッシュ、トランザクション又はアドレスを検索する", 29 | "Size (KB)": "サイズ(バイト)", 30 | "%s of %s Transactions": "トランザクション数 %0 / %1", 31 | "Spent by": "使用者", 32 | "Status": "ステータス", 33 | "%s Transactions": [ 34 | "%0 トランザクション", 35 | "%0 トランザクション" 36 | ], 37 | "Timestamp": "タイムスタンプ", 38 | "Transaction": "トランザクション", 39 | "Transaction fees": "トランザクション手数料", 40 | "Transactions": "トランザクション", 41 | "Transaction: %s": "トランザクション: %0", 42 | "Type": "タイプ", 43 | "Unspent": "未使用", 44 | "Version": "バージョン", 45 | "We encountered an error. Please try again later.": "エラーが発生しました。後ほどもう一度お試し下さい。", 46 | "Weight (KWU)": "ブロックウェイト" 47 | } 48 | -------------------------------------------------------------------------------- /lang/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "Address": "주소", 3 | "Address: %s": "주소: %0", 4 | "Block %s": "블록 %0", 5 | "Block #%s: %s": "블록 #%0: %1", 6 | "Coinbase": "코인베이스", 7 | "Details": "자세히", 8 | "Height": "블록 높이", 9 | "In best chain (%s confirmations)": [ 10 | "현 체인 (%0 컨펌)", 11 | "현 체인 (%0 컨펌)" 12 | ], 13 | "Included in Block": "포함되어 있는 블록", 14 | "lang_id": "ko", 15 | "lang_name": "한국어", 16 | "Loading...": "로딩...", 17 | "Load more": "더 보기", 18 | "Lock time": "잠금 시간", 19 | "Next": "다음", 20 | "Nonstandard": "비표준", 21 | "No results found": "검색결과가 없습니다", 22 | "OP_RETURN data": "OP_RETURN 데이터", 23 | "Output in parent chain": "부모 체인 아웃풋", 24 | "Page Not Found": "페이지를 찾을 수 없음", 25 | "Peg-out": "페그 아웃", 26 | "Peg-out address": "페그 아웃 주소", 27 | "Peg-out ASM": "페그 아웃 ASM", 28 | "Peg-out to": "페그 아웃할 곳", 29 | "Previous": "이전", 30 | "Search for block height, hash, transaction, or address": "블록 높이, 해시, 주소, 또는 거래로 검색해 보세요", 31 | "Size (KB)": "사이즈 (KB)", 32 | "%s of %s Transactions": "%0 의 %1 거래들", 33 | "Spent by": "사용한 유저", 34 | "%s (%s sat/vB)": "%0 (%1 사토시/vB)", 35 | "Status": "상태", 36 | "%s Transactions": [ 37 | "%0 거래들", 38 | "%0 거래들" 39 | ], 40 | "Timestamp": "타임 스탬프", 41 | "Transaction": "거래", 42 | "Transaction fees": "수수료", 43 | "Transactions": "거래", 44 | "Transaction: %s": "거래: %0", 45 | "Type": "유형", 46 | "Unspent": "사용안됨", 47 | "Version": "버전", 48 | "We encountered an error. Please try again later.": "오류가 발생했습니다. 나중에 다시 시도하십시시오.", 49 | "Weight (KWU)": "무게 (KWU)", 50 | "Witness": "서명" 51 | } 52 | -------------------------------------------------------------------------------- /lang/me.json: -------------------------------------------------------------------------------- 1 | { 2 | "Address": "Adresa", 3 | "Address: %s": "Adresa: %0", 4 | "Block %s": "blok %0", 5 | "Block #%s: %s": "Blok #%0: %1", 6 | "Confidential": "Poverljivo", 7 | "Details": "Detalji", 8 | "Height": "Visina", 9 | "In best chain (%s confirmations)": [ 10 | "U najnoljem lancu (1 conferma)", 11 | "U najnoljem lancu (%0 conferme)" 12 | ], 13 | "Included in Block": "Ukljuceno u blok", 14 | "lang_id": "me", 15 | "lang_name": "Црногорски", 16 | "Loading...": "Ucitavanje...", 17 | "Load more": "Ucitaj jos", 18 | "Next": "Sledece", 19 | "No results found": "Nema rezultata", 20 | "Page Not Found": "Strana nije pronadjena", 21 | "Previous": "Prethodni", 22 | "%s Confirmations": [ 23 | "1 Potvrda", 24 | "%0 Potvrda" 25 | ], 26 | "Search for block height, hash, transaction, or address": "Pretraga po visini bloka, hash, transakciji ili adresi", 27 | "Size (KB)": "Velicina (KB)", 28 | "%s of %s Transactions": "%0 od %1 Transakicja", 29 | "Spent by": "Potroseni od strane", 30 | "%s Transactions": [ 31 | "%0 Transakcija", 32 | "%0 Transakcija" 33 | ], 34 | "Timestamp": "Vreme", 35 | "Transaction": "Transakcija", 36 | "Transactions": "Transakcije", 37 | "Transaction: %s": "Transakcija: %0", 38 | "Type": "Tip", 39 | "Unconfirmed": "Nema Potvrda", 40 | "Unspent": "Nepotroseni", 41 | "Version": "Verzija", 42 | "Weight (KWU)": "Tezina (KWU)" 43 | } 44 | -------------------------------------------------------------------------------- /lang/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Address": "Adres", 3 | "Address: %s": "Adres: %0", 4 | "Block %s": "Blokken %0", 5 | "Block #%s: %s": "Blok #%0: %1", 6 | "Confidential": "Vertrouwelijk", 7 | "Height": "Hoogte", 8 | "In best chain (%s confirmations)": [ 9 | "In de beste keten (1 bevestigd)", 10 | "In de beste keten (%0 bevestigd)" 11 | ], 12 | "Included in Block": "Bevind zich in Blok", 13 | "lang_id": "nl", 14 | "lang_name": "Nederlands", 15 | "Loading...": "Bezig met laden...", 16 | "Load more": "Meer laden", 17 | "Next": "Volgende", 18 | "No results found": "Geen Resultaten Gevonden", 19 | "Page Not Found": "Pagina Niet Gevonden", 20 | "Previous": "Vorige", 21 | "%s Confirmations": [ 22 | "1 Bevestiging", 23 | "%0 Bevestigd" 24 | ], 25 | "Search for block height, hash, transaction, or address": "Zoek naar blokhoogte, hash, transactie of adres", 26 | "Size (KB)": "Grootte (KB)", 27 | "%s of %s Transactions": "%0 van %1 Transacties", 28 | "%s Transactions": [ 29 | "%0 Transacties", 30 | "%0 Transacties" 31 | ], 32 | "Transaction": "Transacties", 33 | "Transaction fees": "Kosten", 34 | "Transactions": "Transacties", 35 | "Transaction: %s": "Transactie: %0", 36 | "Unconfirmed": "Niet Bevestigd", 37 | "Version": "Versie" 38 | } 39 | -------------------------------------------------------------------------------- /lang/pt-pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "Address": "Endereço", 3 | "Address: %s": "Endereço: %0", 4 | "Block %s": "Bloco %0", 5 | "Block #%s: %s": "Bloco #%0: %1", 6 | "Confidential": "Confidencial", 7 | "Details": "Detalhes", 8 | "Height": "Altura", 9 | "In best chain (%s confirmations)": [ 10 | "Na melhor cadeia (1 Confirmação)", 11 | "Na melhor cadeia (%0 confirmações)" 12 | ], 13 | "Included in Block": "Incluída no Bloco", 14 | "lang_id": "pt-pt", 15 | "lang_name": "Português", 16 | "Loading...": "Carregando...", 17 | "Load more": "Mais", 18 | "Next": "Próximo", 19 | "No results found": "Nenhum resultado encontrado", 20 | "Page Not Found": "Página Não Encontrada", 21 | "Previous": "Anterior", 22 | "%s Confirmations": [ 23 | "1 Confirmação", 24 | "%0 Confirmações" 25 | ], 26 | "Search for block height, hash, transaction, or address": "Pesquise por altura do bloco, hash, transação ou endereço", 27 | "Size (KB)": "Tamanho (KB)", 28 | "%s of %s Transactions": "%0 de %1 Confirmações", 29 | "Spent by": "Gasto por", 30 | "%s Transactions": [ 31 | "%0 Transações", 32 | "%0 Transações" 33 | ], 34 | "Timestamp": "Registo de data/hora", 35 | "Transaction": "Transação", 36 | "Transaction fees": "Taxas de transação", 37 | "Transactions": "Transações", 38 | "Transaction: %s": "Transação: %0", 39 | "Type": "Tipo", 40 | "Unconfirmed": "Não confirmada", 41 | "Unspent": "Não gasto", 42 | "Version": "Versão", 43 | "Weight (KWU)": "Peso (KWU)" 44 | } 45 | -------------------------------------------------------------------------------- /lang/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "Address": "Адрес", 3 | "Block %s": "Процент (%0) Блока", 4 | "Details": "Подробные детали", 5 | "Height": "Высота", 6 | "In best chain (%s confirmations)": [ 7 | "В лучшей цепи (процент потверждений)", 8 | "В лучшей цепи (процент потверждений)" 9 | ], 10 | "Included in Block": "Включенный в Блоке", 11 | "lang_id": "ru", 12 | "lang_name": "Русский", 13 | "Loading...": "Идёт Загрузка...", 14 | "Load more": "Смотреть больше", 15 | "Lock time": "Время блокировки", 16 | "Next": "Следующий", 17 | "Nonstandard": "Нестандартный", 18 | "No results found": "Результаты Не Найдены", 19 | "OP_RETURN data": "Данные OP_RETURN", 20 | "Output in parent chain": "Выходные данные в родительской цепи", 21 | "Page Not Found": "Страница Не Найдена", 22 | "Peg-out": "Закрепить", 23 | "Peg-out address": "Закрепительный адрес", 24 | "Peg-out to": "Закрепить к", 25 | "Previous": "Предыдущий", 26 | "Search for block height, hash, transaction, or address": "Поиск высоты блока, хэшов, транзакций или адресов", 27 | "Size (KB)": "Объем (Кбайт)", 28 | "%s of %s Transactions": "%0 от %1 Транзакций", 29 | "Spent by": "Потраченный", 30 | "Status": "Статус", 31 | "%s Transactions": [ 32 | "Процент (%0) Транзакций", 33 | "Процент (%0) Транзакций" 34 | ], 35 | "Timestamp": "Штамп Времени", 36 | "Transaction": "Транзакция", 37 | "Transaction fees": "Комиссия за транзакцию", 38 | "Transactions": "Транзакции", 39 | "Type": "Тип", 40 | "Unspent": "Неизрасходованные", 41 | "Version": "Версия", 42 | "We encountered an error. Please try again later.": "Мы столкнулись с ошибкой. Пожалуйста, попробуйте позже.", 43 | "Weight (KWU)": "Вес (kWU)", 44 | "Witness": "Свидетельство" 45 | } 46 | -------------------------------------------------------------------------------- /lang/sr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Address": "Adresa", 3 | "Address: %s": "Adresa: %0", 4 | "Block %s": "blok %0", 5 | "Block #%s: %s": "Blok #%0: %1", 6 | "Confidential": "Poverljivo", 7 | "Details": "Detalji", 8 | "Height": "Visina", 9 | "In best chain (%s confirmations)": [ 10 | "U najnoljem lancu (1 conferma)", 11 | "U najnoljem lancu (%0 conferme)" 12 | ], 13 | "Included in Block": "Ukljuceno u blok", 14 | "lang_id": "sr", 15 | "lang_name": "Српски", 16 | "Loading...": "Ucitavanje...", 17 | "Load more": "Ucitaj jos", 18 | "Next": "Sledece", 19 | "No results found": "Nema rezultata", 20 | "Page Not Found": "Strana nije pronadjena", 21 | "Previous": "Prethodni", 22 | "%s Confirmations": [ 23 | "1 Potvrda", 24 | "%0 Potvrda" 25 | ], 26 | "Search for block height, hash, transaction, or address": "Pretraga po visini bloka, hash, transakciji ili adresi", 27 | "Size (KB)": "Velicina (KB)", 28 | "%s of %s Transactions": "%0 od %1 Transakicja", 29 | "Spent by": "Potroseni od strane", 30 | "%s Transactions": [ 31 | "%0 Transakcija", 32 | "%0 Transakcija" 33 | ], 34 | "Timestamp": "Vreme", 35 | "Transaction": "Transakcija", 36 | "Transaction fees": "Komosija za kopanje", 37 | "Transactions": "Transakcije", 38 | "Transaction: %s": "Transakcija: %0", 39 | "Type": "Tip", 40 | "Unconfirmed": "Nema Potvrda", 41 | "Unspent": "Nepotroseni", 42 | "Version": "Verzija", 43 | "Weight (KWU)": "Tezina (KWU)" 44 | } 45 | -------------------------------------------------------------------------------- /lang/sv.json: -------------------------------------------------------------------------------- 1 | { 2 | "Address": "Adress", 3 | "Address: %s": "Adress: %0", 4 | "Block %s": "Block %0", 5 | "Block #%s: %s": "Böpcl #%0: %1", 6 | "Confidential": "Konfidentiell", 7 | "Details": "Detaljer", 8 | "Height": "Höjd", 9 | "In best chain (%s confirmations)": [ 10 | "I bästa kedjan (1 bekräftelse)", 11 | "I bästa kedjan (%0 bekräftelse" 12 | ], 13 | "Included in Block": "Inkluderad i block", 14 | "lang_id": "sv", 15 | "lang_name": "Svenska", 16 | "Loading...": "Laddar...", 17 | "Load more": "Ladda fler", 18 | "Next": "Nästa", 19 | "No results found": "Inga resultat hittade", 20 | "Page Not Found": "Sida inte hittad", 21 | "Previous": "Föregående", 22 | "%s Confirmations": [ 23 | "1 bekräftelse", 24 | "%0 bekräftelser" 25 | ], 26 | "Search for block height, hash, transaction, or address": "Sök efter blockhöjd, hash, transaktion eller adress", 27 | "Size (KB)": "Storlek (KB)", 28 | "%s of %s Transactions": "%0 av %1 transaktioner", 29 | "Spent by": "Spenderat av", 30 | "%s Transactions": [ 31 | "%0 transaktioner", 32 | "%0 transaktioner" 33 | ], 34 | "Timestamp": "Tidpunkt", 35 | "Transaction": "Transaktion", 36 | "Transaction fees": "Miningavgift", 37 | "Transactions": "Transaktioner", 38 | "Transaction: %s": "Transaktion: %0", 39 | "Type": "Typ", 40 | "Unconfirmed": "Obekräftad", 41 | "Unspent": "Ospenderad", 42 | "Weight (KWU)": "Vikt (KWU)" 43 | } 44 | -------------------------------------------------------------------------------- /lang/util/build-all-json.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -xeo pipefail 4 | 5 | for file in lang/*.po; do 6 | ./lang/util/po2json.js < $file > ${file%.*}.json 7 | done 8 | -------------------------------------------------------------------------------- /lang/util/build-all-po.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is only meant to be run once, initially. 4 | # After that, the canonical files are the .po files and the 5 | # .json files should no longer be updated. 6 | 7 | for file in lang/*.json; do 8 | ./lang/util/json2po.js < $file > ${file%.*}.po 9 | done 10 | -------------------------------------------------------------------------------- /lang/util/extract-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for file in client/src/{,**/}*.js; do ./lang/util/extract.js < $file; done 4 | -------------------------------------------------------------------------------- /lang/util/extract.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs') 4 | , parser = require('@babel/parser') 5 | , traverse = require('@babel/traverse').default 6 | 7 | const jsStr = fs.readFileSync('/dev/stdin').toString() 8 | , jsAst = parser.parse(jsStr, { sourceType: 'module', plugins: [ "jsx" ] }) 9 | 10 | // Extracts translation strings from source files by looking 11 | // for t`...` template strings and for string literals that appear like UI strings 12 | 13 | // literal strings starting with an uppercase and containing at least one space 14 | // are considered to be user-presented UI strings 15 | const reLangStr = /^[A-Z].* / 16 | 17 | // Except for these, which match the pattern but don't need translations 18 | const langStrBlacklist = ['Block Explorer', 'Request has been terminated', 'Toggle navigation'] 19 | 20 | traverse(jsAst, { 21 | TemplateLiteral(path) { 22 | if (path.container.tag && path.container.tag.name == 't') { 23 | const parts = path.node.quasis.map(p => p.value.raw) 24 | console.log(parts.join('%s')) 25 | } 26 | }, 27 | 28 | StringLiteral(path) { 29 | if (reLangStr.test(path.node.value) && !langStrBlacklist.includes(path.node.value)) { 30 | console.log(path.node.value) 31 | } 32 | } 33 | }) 34 | 35 | -------------------------------------------------------------------------------- /lang/util/fetch-transifex.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | TRANSIFEX_PROJECT=esplora 5 | TRANSIFEX_RESOURCE=esplora 6 | 7 | for file in lang/*.po; do 8 | lang=`basename $file` 9 | lang=${lang%.*} 10 | lang=${lang%-*} 11 | 12 | # These are identified differently by transifex 13 | [ "$lang" == "me" ] && lang=sr_ME 14 | [ "$lang" == "jp" ] && lang=ja 15 | 16 | echo "Downloading $lang from transifex to $file" 17 | curl -s -L -u api:$TRANSIFEX_KEY "https://www.transifex.com/api/2/project/$TRANSIFEX_PROJECT/resource/$TRANSIFEX_RESOURCE/translation/$lang?file=po&mode=reviewed" > $file 18 | 19 | echo "Generating json from $file" 20 | ./lang/util/po2json.js < $file > ${file%.*}.json 21 | done 22 | -------------------------------------------------------------------------------- /lang/util/json2po.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs') 4 | , path = require('path') 5 | 6 | const escape = str => !str ? '' : str.replace(/[\\"]/g, "\\$&") 7 | 8 | const lang_strs = JSON.parse(fs.readFileSync('/dev/stdin')) 9 | , lang_id = lang_strs.lang_id 10 | , eng_strs = require('../en.json') 11 | 12 | const poStr = fs.readFileSync(path.join(__dirname, '..', 'strings.txt')).toString('utf8') 13 | .split("\n") 14 | .filter(Boolean) 15 | .map(str => { 16 | 17 | const has_plural = Array.isArray(eng_strs[str]) 18 | let lang_str = lang_strs[str] || (lang_id == 'en' && str) 19 | 20 | if (has_plural && !Array.isArray(lang_str)) { 21 | lang_str = [ lang_str, lang_str ] 22 | } 23 | 24 | return !lang_str ? null 25 | : !has_plural ? `msgid "${str}"\nmsgstr "${escape(lang_str)}"` 26 | : `msgid "${escape(str)} (singular)"\nmsgstr "${escape(lang_str[0])}"\n\n` 27 | +`msgid "${escape(str)} (plural)"\nmsgstr "${escape(lang_str[1])}"` 28 | }) 29 | .filter(Boolean) 30 | .join("\n\n") 31 | 32 | console.log(` 33 | msgid "" 34 | msgstr "Language: ${lang_id}\\n" 35 | 36 | ${poStr} 37 | `) 38 | -------------------------------------------------------------------------------- /lang/util/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esplora-lang-utils", 3 | "dependencies": { 4 | "@babel/parser": "^7.12.11", 5 | "@babel/traverse": "^7.12.10" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lang/util/po2json.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const reSingular = / \(singular\)$/ 4 | , rePlural = / \(plural\)$/ 5 | 6 | require('pofile').load('/dev/stdin', (err, po) => { 7 | if (err) throw err 8 | 9 | const tran = po.items.filter(item => item.msgstr[0] != '' || item.msgstr.length > 1) 10 | .reduce((T, { msgid, msgstr }) => { 11 | if (msgstr.length && !(msgstr.length == 1 && msgid === msgstr[0])) { 12 | const isSingular = reSingular.test(msgid), isPlural = rePlural.test(msgid) 13 | if (isSingular || isPlural) { 14 | const msgid_s = msgid.replace(reSingular, '').replace(rePlural, '') 15 | T[msgid_s] || (T[msgid_s] = []) 16 | T[msgid_s][isSingular?0:1] = msgstr[0] 17 | } else { 18 | T[msgid] = msgstr[0] 19 | } 20 | } 21 | return T 22 | }, {}) 23 | 24 | console.log(JSON.stringify(tran, null, 2)) 25 | }) 26 | -------------------------------------------------------------------------------- /lang/util/update-strings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # requires sponge from moreutils 4 | ( 5 | cat lang/strings.txt 6 | ./lang/util/extract-all.sh 7 | ) | sort | uniq | sponge lang/strings.txt 8 | -------------------------------------------------------------------------------- /lang/zh-cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "Address": "地址", 3 | "Address: %s": "地址: %0", 4 | "Block height": "区块高度", 5 | "Block %s": "块 %0", 6 | "Block #%s: %s": "区块 #%0: %1", 7 | "Confidential": "保密", 8 | "Details": "细节", 9 | "Height": "块高度", 10 | "In best chain (%s confirmations)": [ 11 | "在最正式的链 (确认数 1)", 12 | "在最正式的链 (确认数 %0)" 13 | ], 14 | "Included in Block": "在区块", 15 | "lang_id": "zh-cn", 16 | "lang_name": "中文(简体)", 17 | "Loading...": "页面加载...", 18 | "Load more": "更多", 19 | "Next": "后一个块", 20 | "No results found": "未找到结果", 21 | "Page Not Found": "网页未找到", 22 | "Previous": "前一个块", 23 | "%s Confirmations": [ 24 | "确认数 1", 25 | "确认数 %0" 26 | ], 27 | "Search for block height, hash, transaction, or address": "高度、哈希、交易或地址...", 28 | "Size (KB)": "大小 (KB)", 29 | "%s of %s Transactions": "交易 %0 / %1", 30 | "Status": "状态", 31 | "%s Transactions": [ 32 | "交易数量 %0", 33 | "交易数量 %0" 34 | ], 35 | "Timestamp": "时间戳", 36 | "Transaction": "交易", 37 | "Transaction fees": "矿工手续费", 38 | "Transactions": "交易", 39 | "Transaction: %s": "交易: %0", 40 | "Type": "类型", 41 | "Unconfirmed": "没确认", 42 | "Version": "版本", 43 | "Weight (KWU)": "重量 (KWU)" 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esplora", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "dev-server": "babel-node dev-server.js", 6 | "prerender-server": "cd prerender-server && npm start", 7 | "postinstall": "(cd client && npm install) && (cd prerender-server && npm install)", 8 | "dist": "./build.sh", 9 | "l10n:fetch": "./lang/util/fetch-transifex.sh", 10 | "l10n:update-strings": "./lang/util/update-strings.sh" 11 | }, 12 | "author": "Nadav Ivgi", 13 | "license": "MIT", 14 | "browserslist": "> 0.5%", 15 | "devDependencies": { 16 | "browserify-middleware": "^8.1.1", 17 | "express": "^4.17.1", 18 | "glob": "^7.1.6", 19 | "morgan": "^1.10.0" 20 | }, 21 | "dependencies": { 22 | "@babel/cli": "^7.12.10", 23 | "@babel/core": "^7.12.10", 24 | "@babel/node": "^7.12.10", 25 | "@babel/preset-env": "^7.12.11", 26 | "browserify": "^17.0.0", 27 | "bundle-collapser": "^1.4.0", 28 | "cssjanus": "^2.0.1", 29 | "move-decimal-point": "0.0.4", 30 | "patch-package": "^6.2.2", 31 | "pofile": "^1.1.0", 32 | "pug": "^3.0.0", 33 | "pug-cli": "^1.0.0-alpha6", 34 | "snabbdom-to-html": "^6.0.0", 35 | "typeface-inconsolata": "0.0.72", 36 | "uglify-es": "^3.3.9" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /prerender-server/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ../node_modules/.bin/babel --root-mode upward --only src,../client/src -d dist src 4 | (cd ../client && npm run dist) 5 | 6 | rm -f client 7 | ln -s ../client/dist client 8 | -------------------------------------------------------------------------------- /prerender-server/client: -------------------------------------------------------------------------------- 1 | ../client/src -------------------------------------------------------------------------------- /prerender-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esplora-serverside-render", 3 | "version": "0.1.0", 4 | "author": "Nadav Ivgi", 5 | "license": "MIT", 6 | "dependencies": { 7 | "body-parser": "^1.19.0", 8 | "cookie-parser": "^1.4.5", 9 | "superagent": "^6.1.0" 10 | }, 11 | "scripts": { 12 | "start": "./start.sh", 13 | "dist": "./build.sh" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /prerender-server/src/cluster.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster') 2 | , numCPUs = require('os').cpus().length 3 | 4 | if (cluster.isMaster) { 5 | console.log(`Master ${process.pid} is running`); 6 | 7 | for (let i = 0; i < numCPUs; i++) { 8 | cluster.fork() 9 | } 10 | 11 | cluster.on('exit', (worker, code, signal) => { 12 | console.log(`worker ${worker.process.pid} died`, { worker, code, signal }); 13 | }); 14 | } else { 15 | require('./server') 16 | console.log(`Worker ${process.pid} started`); 17 | } 18 | -------------------------------------------------------------------------------- /prerender-server/src/server.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import pug from 'pug' 3 | import path from 'path' 4 | import express from 'express' 5 | import request from 'superagent' 6 | 7 | import l10n from '../client/l10n' 8 | import render from '../client/run-server' 9 | 10 | const themes = [ 'light', 'dark' ] 11 | , langs = Object.keys(l10n) 12 | , baseHref = process.env.BASE_HREF || '/' 13 | , canonBase = process.env.CANONICAL_URL ? process.env.CANONICAL_URL.replace(/\/$/, '') : null 14 | , apiUrl = process.env.API_URL.replace(/\/$/, '') 15 | 16 | const rpath = p => path.join(__dirname, p) 17 | 18 | const indexView = rpath('../../client/index.pug') 19 | 20 | const app = express() 21 | app.engine('pug', pug.__express) 22 | 23 | if (app.settings.env == 'development') 24 | app.use(require('morgan')('dev')) 25 | 26 | app.use(require('cookie-parser')()) 27 | app.use(require('body-parser').urlencoded({ extended: false })) 28 | 29 | app.use((req, res, next) => { 30 | // TODO: optimize /block-height/nnn (no need to render the whole app just to get the redirect) 31 | 32 | let theme = req.query.theme || req.cookies.theme || 'dark' 33 | if (!themes.includes(theme)) theme = 'light' 34 | if (req.query.theme && req.cookies.theme !== theme) res.cookie('theme', theme) 35 | 36 | let lang = req.query.lang || req.cookies.lang || 'en' 37 | if (!langs.includes(lang)) lang = 'en' 38 | if (req.query.lang && req.cookies.lang !== lang) res.cookie('lang', lang) 39 | 40 | render(req._parsedUrl.pathname, req._parsedUrl.query || '', req.body, { theme, lang }, (err, resp) => { 41 | if (err) return next(err) 42 | if (resp.redirect) return res.redirect(301, baseHref + resp.redirect.substr(1)) 43 | if (resp.errorCode) { 44 | console.error(`Failed with code ${resp.errorCode}:`, resp) 45 | return res.sendStatus(resp.errorCode) 46 | } 47 | 48 | res.status(resp.status || 200) 49 | res.render(indexView, { 50 | prerender_title: resp.title 51 | , prerender_html: resp.html 52 | , canon_url: canonBase ? canonBase + req.url : null 53 | , noscript: true 54 | , theme 55 | , t: l10n[lang] 56 | }) 57 | }) 58 | 59 | }) 60 | 61 | // Cleanup socket file from previous executions 62 | if (process.env.SOCKET_PATH) { 63 | try { 64 | if (fs.statSync(process.env.SOCKET_PATH).isSocket()) { 65 | fs.unlinkSync(process.env.SOCKET_PATH) 66 | } 67 | } catch (_) {} 68 | } 69 | 70 | app.listen(process.env.SOCKET_PATH || process.env.PORT || 5001, function(){ 71 | let addr = this.address() 72 | if (addr.address) addr = `${addr.address}:${addr.port}` 73 | console.log(`HTTP server running on ${addr}`) 74 | }) 75 | -------------------------------------------------------------------------------- /prerender-server/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | [[ -d node_modules ]] || npm install 4 | 5 | if [ "$1" == "--cluster" ]; then 6 | file=cluster 7 | shift 8 | else 9 | file=server 10 | fi 11 | 12 | for flavor in "$@"; do source ../flavors/$flavor/config.env; done 13 | 14 | if [ -d dist ] && [ "$NODE_ENV" != "development" ]; then 15 | node dist/$file.js "$@" 16 | else 17 | babel-node --root-mode upward --only src,../client/src src/$file.js "$@" 18 | fi 19 | -------------------------------------------------------------------------------- /render-view.js: -------------------------------------------------------------------------------- 1 | global.window = {} 2 | 3 | const pug = require('pug') 4 | , l10n = require('./client/src/l10n').default 5 | , state = JSON.parse(process.argv[2]) 6 | , view = require('./client/src/views')[state.view] 7 | 8 | state.t = l10n[state.lang || 'en'] 9 | state.page = { pathname: '', query: {} } 10 | 11 | require('pug').renderFile('client/index.pug', { 12 | prerender_html: require('snabbdom-to-html')(view(state)) 13 | , theme: 'dark' 14 | }, (err, html) => { 15 | if (err) throw err 16 | console.log(html) 17 | }) 18 | -------------------------------------------------------------------------------- /www/bootstrap.min.css: -------------------------------------------------------------------------------- 1 | ../client/node_modules/bootstrap/dist/css/bootstrap.min.css -------------------------------------------------------------------------------- /www/font/ChakraPetch-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/font/ChakraPetch-Bold.ttf -------------------------------------------------------------------------------- /www/font/Inter-VariableFont_opsz,wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/font/Inter-VariableFont_opsz,wght.ttf -------------------------------------------------------------------------------- /www/font/SourceSansPro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/font/SourceSansPro-Regular.ttf -------------------------------------------------------------------------------- /www/font/SourceSansPro-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/font/SourceSansPro-SemiBold.ttf -------------------------------------------------------------------------------- /www/font/inconsolata: -------------------------------------------------------------------------------- 1 | ../../node_modules/typeface-inconsolata -------------------------------------------------------------------------------- /www/img/Loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/Loading.gif -------------------------------------------------------------------------------- /www/img/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/block.png -------------------------------------------------------------------------------- /www/img/ellipse-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /www/img/ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /www/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/favicon.png -------------------------------------------------------------------------------- /www/img/github_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/github_blue.png -------------------------------------------------------------------------------- /www/img/icons/Bitcoin Testnet-menu-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/Bitcoin Testnet-menu-logo.png -------------------------------------------------------------------------------- /www/img/icons/Bitcoin Testnet-menu-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | btc_test 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /www/img/icons/Bitcoin-menu-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | btc_main 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /www/img/icons/BitcoinTestnet-menu-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | btc_test 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /www/img/icons/Liquid-menu-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | lbtc_main 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /www/img/icons/LiquidTestnet-menu-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | lbtc_test 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /www/img/icons/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/apple.png -------------------------------------------------------------------------------- /www/img/icons/apple_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/apple_dark.png -------------------------------------------------------------------------------- /www/img/icons/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/arrow.png -------------------------------------------------------------------------------- /www/img/icons/arrow_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/arrow_down.png -------------------------------------------------------------------------------- /www/img/icons/arrow_left_blu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/arrow_left_blu.png -------------------------------------------------------------------------------- /www/img/icons/arrow_right_blu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/arrow_right_blu.png -------------------------------------------------------------------------------- /www/img/icons/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/cancel.png -------------------------------------------------------------------------------- /www/img/icons/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/code.png -------------------------------------------------------------------------------- /www/img/icons/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/copy.png -------------------------------------------------------------------------------- /www/img/icons/database.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /www/img/icons/encryption.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /www/img/icons/google-play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/google-play.png -------------------------------------------------------------------------------- /www/img/icons/google-play_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/google-play_dark.png -------------------------------------------------------------------------------- /www/img/icons/integration.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /www/img/icons/issuance.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /www/img/icons/lbtc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /www/img/icons/left-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/left-arrow.png -------------------------------------------------------------------------------- /www/img/icons/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/linux.png -------------------------------------------------------------------------------- /www/img/icons/linux_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/linux_dark.png -------------------------------------------------------------------------------- /www/img/icons/menu-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | btc_main 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /www/img/icons/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/minus.png -------------------------------------------------------------------------------- /www/img/icons/moon_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/moon_dark.png -------------------------------------------------------------------------------- /www/img/icons/moon_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/moon_light.png -------------------------------------------------------------------------------- /www/img/icons/old-minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/old-minus.png -------------------------------------------------------------------------------- /www/img/icons/old-plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/old-plus.png -------------------------------------------------------------------------------- /www/img/icons/peg_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/peg_in.png -------------------------------------------------------------------------------- /www/img/icons/peg_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/peg_out.png -------------------------------------------------------------------------------- /www/img/icons/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/plus.png -------------------------------------------------------------------------------- /www/img/icons/pricing1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /www/img/icons/pricing2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /www/img/icons/privacy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /www/img/icons/qrcode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Icon/qr 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /www/img/icons/rest-api.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /www/img/icons/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/search.png -------------------------------------------------------------------------------- /www/img/icons/security-tokens.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /www/img/icons/switch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/icons/switch-icon.png -------------------------------------------------------------------------------- /www/img/icons/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /www/img/logos/aqua-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /www/img/logos/aqua.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /www/img/logos/nunchuk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /www/img/logos/sparrow-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/logos/sparrow-dark.png -------------------------------------------------------------------------------- /www/img/logos/sparrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/logos/sparrow.png -------------------------------------------------------------------------------- /www/img/onion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/onion.png -------------------------------------------------------------------------------- /www/img/onion_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/onion_light.png -------------------------------------------------------------------------------- /www/img/transaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blockstream/esplora/9873fa7f15540b901e4bed9638d83265a117a334/www/img/transaction.png -------------------------------------------------------------------------------- /www/instascan.min.js: -------------------------------------------------------------------------------- 1 | ../client/node_modules/instascan/dist/instascan.min.js -------------------------------------------------------------------------------- /www/js/infinite-scroll.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | let animationFrame; 3 | 4 | function initInfiniteScroll() { 5 | if (animationFrame) { 6 | cancelAnimationFrame(animationFrame); 7 | } 8 | 9 | function setupScroll() { 10 | if (window.innerWidth > 820) { 11 | return; 12 | } 13 | 14 | const track = document.querySelector('.logos-track'); 15 | if (!track) { 16 | setTimeout(setupScroll, 100); 17 | return; 18 | } 19 | 20 | const originalLogos = Array.from(track.children); 21 | const originalWidth = track.scrollWidth; 22 | 23 | while (track.scrollWidth < originalWidth * 3) { 24 | originalLogos.forEach(logo => { 25 | const clone = logo.cloneNode(true); 26 | track.appendChild(clone); 27 | }); 28 | } 29 | 30 | let position = 0; 31 | const speed = 0.5; 32 | 33 | function animate() { 34 | position += speed; 35 | const resetPoint = originalWidth; 36 | 37 | if (position >= resetPoint) { 38 | position = 0; 39 | track.style.transition = 'none'; 40 | track.style.transform = `translateX(0)`; 41 | track.offsetHeight; 42 | track.style.transition = 'transform 0.1s linear'; 43 | } 44 | 45 | track.style.transform = `translateX(-${position}px)`; 46 | animationFrame = requestAnimationFrame(animate); 47 | } 48 | 49 | track.style.transition = 'none'; 50 | track.style.transform = 'translateX(0)'; 51 | track.offsetHeight; // Force reflow 52 | track.style.transition = 'transform 0.1s linear'; 53 | 54 | animationFrame = requestAnimationFrame(animate); 55 | } 56 | 57 | setupScroll(); 58 | 59 | let resizeTimeout; 60 | window.addEventListener('resize', () => { 61 | clearTimeout(resizeTimeout); 62 | resizeTimeout = setTimeout(setupScroll, 150); 63 | }); 64 | } 65 | 66 | if (document.readyState === 'loading') { 67 | document.addEventListener('DOMContentLoaded', initInfiniteScroll); 68 | } else { 69 | initInfiniteScroll(); 70 | } 71 | 72 | window.initInfiniteScroll = initInfiniteScroll; 73 | })(); -------------------------------------------------------------------------------- /www/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /nojs/ --------------------------------------------------------------------------------