├── .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 |
--------------------------------------------------------------------------------
/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 |
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(, { 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 | Target | sat/vB | Mempool depth |
52 | { sortEst(feeEst).map(([ target, feerate ]) =>
53 | {t`${target} blocks`} | {feerate.toFixed(2)} | {t`${formatVMB(getMempoolDepth(mempool.fee_histogram, feerate))} from tip`} |
54 | )}
55 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
6 |
--------------------------------------------------------------------------------
/flavors/blockstream/www/img/icons/light-logo-icon.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/www/img/ellipse.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------
/www/img/icons/Bitcoin-menu-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/www/img/icons/BitcoinTestnet-menu-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/www/img/icons/Liquid-menu-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/www/img/icons/LiquidTestnet-menu-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
26 |
--------------------------------------------------------------------------------
/www/img/icons/encryption.svg:
--------------------------------------------------------------------------------
1 |
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 |
28 |
--------------------------------------------------------------------------------
/www/img/icons/issuance.svg:
--------------------------------------------------------------------------------
1 |
25 |
--------------------------------------------------------------------------------
/www/img/icons/lbtc.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------
/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 |
6 |
--------------------------------------------------------------------------------
/www/img/icons/pricing2.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/www/img/icons/privacy.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/www/img/icons/qrcode.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/www/img/icons/rest-api.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
4 |
--------------------------------------------------------------------------------
/www/img/logos/aqua-dark.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/www/img/logos/aqua.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/www/img/logos/nunchuk.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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/
--------------------------------------------------------------------------------