diff --git a/.gitignore b/.gitignore
index 12ed6d50ad..31e469449a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+configs/environ.overrides.json
+configs/environ.override.json
+
vendor/
data*/
profile/
@@ -19,4 +22,4 @@ __debug*
.gocache/
__pycache__/
.dev/
-.spec/
\ No newline at end of file
+.spec/
diff --git a/api/embed/about b/api/embed/about
deleted file mode 100644
index 79156218b0..0000000000
--- a/api/embed/about
+++ /dev/null
@@ -1 +0,0 @@
-Blockbook - blockchain indexer for Trezor Suite https://trezor.io/trezor-suite. Do not use for any other purpose.
diff --git a/api/embed/tos_link b/api/embed/tos_link
deleted file mode 100644
index 8c90089b92..0000000000
--- a/api/embed/tos_link
+++ /dev/null
@@ -1 +0,0 @@
-https://trezor.io/terms-of-use
diff --git a/api/text.go b/api/text.go
index 57efebbad5..11a09418b3 100644
--- a/api/text.go
+++ b/api/text.go
@@ -1,34 +1,15 @@
package api
-import (
- "embed"
- "fmt"
- "net/url"
- "strings"
-)
+import "strings"
-//go:embed embed/*
-var embedded embed.FS
-
-// Text contains static overridable texts used in explorer
+// Text contains static overridable texts used in explorer and API.
var Text struct {
BlockbookAbout, TOSLink string
}
-func init() {
- if about, err := embedded.ReadFile("embed/about"); err == nil {
- Text.BlockbookAbout = strings.TrimSpace(string(about))
- } else {
- panic(err)
- }
- if tosLinkB, err := embedded.ReadFile("embed/tos_link"); err == nil {
- tosLink := strings.TrimSpace(string(tosLinkB))
- if _, err := url.ParseRequestURI(tosLink); err == nil {
- Text.TOSLink = tosLink
- } else {
- panic(fmt.Sprint("tos_link is not valid URL:", err.Error()))
- }
- } else {
- panic(err)
- }
+// SetBranding sets BlockbookAbout and TOSLink from branding loaded at process startup.
+// Callers must pass values already validated by common.LoadBranding (about non-empty, tos_link a valid absolute URL).
+func SetBranding(about, tosLink string) {
+ Text.BlockbookAbout = strings.TrimSpace(about)
+ Text.TOSLink = strings.TrimSpace(tosLink)
}
diff --git a/blockbook.go b/blockbook.go
index 8205a14fa2..f71178398a 100644
--- a/blockbook.go
+++ b/blockbook.go
@@ -8,6 +8,7 @@ import (
"net/http"
_ "net/http/pprof"
"os"
+ "path/filepath"
"os/signal"
"runtime/debug"
"strconv"
@@ -108,6 +109,7 @@ var (
callbacksOnNewTx []bchain.OnNewTxFunc
callbacksOnNewFiatRatesTicker []fiat.OnNewFiatRatesTicker
chanOsSignal chan os.Signal
+ appBranding *common.Branding
)
func init() {
@@ -176,6 +178,14 @@ func mainWithExitCode() int {
return exitCodeFatal
}
+ configsDir := filepath.Clean(filepath.Join(filepath.Dir(*configFile), ".."))
+ appBranding, err = common.LoadBranding(configsDir)
+ if err != nil {
+ glog.Error("branding: ", err)
+ return exitCodeFatal
+ }
+ api.SetBranding(appBranding.About, appBranding.TOSLink)
+
metrics, err = common.GetMetrics(config.CoinName)
if err != nil {
glog.Error("metrics: ", err)
@@ -442,7 +452,7 @@ func startInternalServer() (*server.InternalServer, error) {
func startPublicServer() (*server.PublicServer, error) {
// start public server in limited functionality, extend it after sync is finished by calling ConnectFullPublicInterface
- publicServer, err := server.NewPublicServer(*publicBinding, *certFiles, index, chain, mempool, txCache, *explorerURL, metrics, internalState, fiatRates, *debugMode)
+ publicServer, err := server.NewPublicServer(*publicBinding, *certFiles, index, chain, mempool, txCache, *explorerURL, metrics, internalState, fiatRates, *debugMode, appBranding)
if err != nil {
return nil, err
}
diff --git a/common/branding.go b/common/branding.go
new file mode 100644
index 0000000000..fb74af2c96
--- /dev/null
+++ b/common/branding.go
@@ -0,0 +1,301 @@
+package common
+
+import (
+ "encoding/json"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/juju/errors"
+)
+
+// BrandingFooter holds explorer footer labels and hrefs.
+// For each link except terms, leave label and href both empty to hide that slot (after trim).
+// Terms uses tos_link; leave terms_label empty to hide the terms link.
+type BrandingFooter struct {
+ CreatedByLabel string `json:"created_by_label"`
+ CreatedByHref string `json:"created_by_href"`
+ TermsLabel string `json:"terms_label"`
+ BrandLabel string `json:"brand_label"`
+ BrandHref string `json:"brand_href"`
+ SuiteLabel string `json:"suite_label"`
+ SuiteHref string `json:"suite_href"`
+ SupportLabel string `json:"support_label"`
+ SupportHref string `json:"support_href"`
+ SendTxLabel string `json:"send_tx_label"`
+ SendTxHref string `json:"send_tx_href"`
+ PromoLabel string `json:"promo_label"`
+ PromoHref string `json:"promo_href"`
+}
+
+// Branding holds explorer and API branding strings loaded from configs/environ.json
+// with optional overlay from configs/environ.overrides.json.
+type Branding struct {
+ BrandName string `json:"brand_name"`
+ About string `json:"about"`
+ TOSLink string `json:"tos_link"`
+ GithubRepoURL string `json:"github_repo_url"`
+ FiatRatesCredit string `json:"fiat_rates_credit"`
+ LogoURL string `json:"logo_url"`
+ FaviconURL string `json:"favicon_url"`
+ LogoWidthPx int `json:"logo_width_px"`
+ LogoRightPaddingPx int `json:"logo_right_padding_px"`
+ Footer BrandingFooter `json:"footer"`
+}
+
+type environBrandingRoot struct {
+ Branding *Branding `json:"branding"`
+}
+
+// brandingOverride uses pointers so empty strings and partial overrides are applied correctly.
+type brandingOverride struct {
+ BrandName *string `json:"brand_name"`
+ About *string `json:"about"`
+ TOSLink *string `json:"tos_link"`
+ GithubRepoURL *string `json:"github_repo_url"`
+ FiatRatesCredit *string `json:"fiat_rates_credit"`
+ LogoURL *string `json:"logo_url"`
+ FaviconURL *string `json:"favicon_url"`
+ LogoWidthPx *int `json:"logo_width_px"`
+ LogoRightPaddingPx *int `json:"logo_right_padding_px"`
+ Footer *footerBrandingOverride `json:"footer"`
+}
+
+type footerBrandingOverride struct {
+ CreatedByLabel *string `json:"created_by_label"`
+ CreatedByHref *string `json:"created_by_href"`
+ TermsLabel *string `json:"terms_label"`
+ BrandLabel *string `json:"brand_label"`
+ BrandHref *string `json:"brand_href"`
+ SuiteLabel *string `json:"suite_label"`
+ SuiteHref *string `json:"suite_href"`
+ SupportLabel *string `json:"support_label"`
+ SupportHref *string `json:"support_href"`
+ SendTxLabel *string `json:"send_tx_label"`
+ SendTxHref *string `json:"send_tx_href"`
+ PromoLabel *string `json:"promo_label"`
+ PromoHref *string `json:"promo_href"`
+}
+
+// LoadBranding reads configs/environ.json (required branding block) and merges
+// configs/environ.overrides.json when present. Returned branding has non-empty
+// about and a valid absolute tos_link (ParseRequestURI).
+func LoadBranding(configsDir string) (*Branding, error) {
+ basePath := filepath.Join(configsDir, "environ.json")
+ b, err := os.ReadFile(basePath)
+ if err != nil {
+ return nil, errors.Annotatef(err, "read %s", basePath)
+ }
+ var root environBrandingRoot
+ if err := json.Unmarshal(b, &root); err != nil {
+ return nil, errors.Annotatef(err, "parse %s", basePath)
+ }
+ if root.Branding == nil {
+ return nil, errors.Errorf("%s: missing branding", basePath)
+ }
+ out := cloneBranding(root.Branding)
+
+ overridePath := filepath.Join(configsDir, "environ.overrides.json")
+ if st, err := os.Stat(overridePath); err == nil && !st.IsDir() {
+ ob, err := os.ReadFile(overridePath)
+ if err != nil {
+ return nil, errors.Annotatef(err, "read %s", overridePath)
+ }
+ var oroot struct {
+ Branding *brandingOverride `json:"branding"`
+ }
+ if err := json.Unmarshal(ob, &oroot); err != nil {
+ return nil, errors.Annotatef(err, "parse %s", overridePath)
+ }
+ if oroot.Branding != nil {
+ applyBrandingOverride(out, oroot.Branding)
+ }
+ }
+
+ trimBrandingStrings(out)
+
+ if err := validateAbout(out.About); err != nil {
+ return nil, err
+ }
+ if err := validateTOSLink(out.TOSLink); err != nil {
+ return nil, err
+ }
+ if err := validateFooterNav(&out.Footer); err != nil {
+ return nil, err
+ }
+
+ return out, nil
+}
+
+func trimBrandingStrings(b *Branding) {
+ b.About = strings.TrimSpace(b.About)
+ b.BrandName = strings.TrimSpace(b.BrandName)
+ b.TOSLink = strings.TrimSpace(b.TOSLink)
+ b.GithubRepoURL = strings.TrimSpace(b.GithubRepoURL)
+ b.FiatRatesCredit = strings.TrimSpace(b.FiatRatesCredit)
+ b.LogoURL = strings.TrimSpace(b.LogoURL)
+ b.FaviconURL = strings.TrimSpace(b.FaviconURL)
+ f := &b.Footer
+ f.CreatedByLabel = strings.TrimSpace(f.CreatedByLabel)
+ f.CreatedByHref = strings.TrimSpace(f.CreatedByHref)
+ f.TermsLabel = strings.TrimSpace(f.TermsLabel)
+ f.BrandLabel = strings.TrimSpace(f.BrandLabel)
+ f.BrandHref = strings.TrimSpace(f.BrandHref)
+ f.SuiteLabel = strings.TrimSpace(f.SuiteLabel)
+ f.SuiteHref = strings.TrimSpace(f.SuiteHref)
+ f.SupportLabel = strings.TrimSpace(f.SupportLabel)
+ f.SupportHref = strings.TrimSpace(f.SupportHref)
+ f.SendTxLabel = strings.TrimSpace(f.SendTxLabel)
+ f.SendTxHref = strings.TrimSpace(f.SendTxHref)
+ f.PromoLabel = strings.TrimSpace(f.PromoLabel)
+ f.PromoHref = strings.TrimSpace(f.PromoHref)
+}
+
+func cloneBranding(src *Branding) *Branding {
+ if src == nil {
+ return nil
+ }
+ cp := *src
+ return &cp
+}
+
+func applyBrandingOverride(dst *Branding, o *brandingOverride) {
+ if o.BrandName != nil {
+ dst.BrandName = *o.BrandName
+ }
+ if o.About != nil {
+ dst.About = *o.About
+ }
+ if o.TOSLink != nil {
+ dst.TOSLink = *o.TOSLink
+ }
+ if o.GithubRepoURL != nil {
+ dst.GithubRepoURL = *o.GithubRepoURL
+ }
+ if o.FiatRatesCredit != nil {
+ dst.FiatRatesCredit = *o.FiatRatesCredit
+ }
+ if o.LogoURL != nil {
+ dst.LogoURL = *o.LogoURL
+ }
+ if o.FaviconURL != nil {
+ dst.FaviconURL = *o.FaviconURL
+ }
+ if o.LogoWidthPx != nil {
+ dst.LogoWidthPx = *o.LogoWidthPx
+ }
+ if o.LogoRightPaddingPx != nil {
+ dst.LogoRightPaddingPx = *o.LogoRightPaddingPx
+ }
+ if o.Footer != nil {
+ applyFooterOverride(&dst.Footer, o.Footer)
+ }
+}
+
+func applyFooterOverride(dst *BrandingFooter, o *footerBrandingOverride) {
+ if o.CreatedByLabel != nil {
+ dst.CreatedByLabel = *o.CreatedByLabel
+ }
+ if o.CreatedByHref != nil {
+ dst.CreatedByHref = *o.CreatedByHref
+ }
+ if o.TermsLabel != nil {
+ dst.TermsLabel = *o.TermsLabel
+ }
+ if o.BrandLabel != nil {
+ dst.BrandLabel = *o.BrandLabel
+ }
+ if o.BrandHref != nil {
+ dst.BrandHref = *o.BrandHref
+ }
+ if o.SuiteLabel != nil {
+ dst.SuiteLabel = *o.SuiteLabel
+ }
+ if o.SuiteHref != nil {
+ dst.SuiteHref = *o.SuiteHref
+ }
+ if o.SupportLabel != nil {
+ dst.SupportLabel = *o.SupportLabel
+ }
+ if o.SupportHref != nil {
+ dst.SupportHref = *o.SupportHref
+ }
+ if o.SendTxLabel != nil {
+ dst.SendTxLabel = *o.SendTxLabel
+ }
+ if o.SendTxHref != nil {
+ dst.SendTxHref = *o.SendTxHref
+ }
+ if o.PromoLabel != nil {
+ dst.PromoLabel = *o.PromoLabel
+ }
+ if o.PromoHref != nil {
+ dst.PromoHref = *o.PromoHref
+ }
+}
+
+func validateAbout(about string) error {
+ if about == "" {
+ return errors.New("branding about is required")
+ }
+ return nil
+}
+
+func validateTOSLink(tos string) error {
+ if tos == "" {
+ return errors.New("branding tos_link is required")
+ }
+ if _, err := url.ParseRequestURI(tos); err != nil {
+ return errors.Annotate(err, "branding tos_link is not a valid URL")
+ }
+ return nil
+}
+
+func validateFooterNavHref(field, h string) error {
+ if h == "" {
+ return errors.Errorf("branding %s is required", field)
+ }
+ if strings.HasPrefix(h, "/") && !strings.HasPrefix(h, "//") {
+ return nil
+ }
+ if _, err := url.ParseRequestURI(h); err != nil {
+ return errors.Annotatef(err, "branding %s is not a valid URL", field)
+ }
+ return nil
+}
+
+// validateOptionalFooterPair requires label and href to be both set or both empty.
+// An empty pair omits that footer link in templates (overrides can hide slots).
+func validateOptionalFooterPair(labelField, hrefField, label, href string) error {
+ labelEmpty := label == ""
+ hrefEmpty := href == ""
+ if labelEmpty && hrefEmpty {
+ return nil
+ }
+ if labelEmpty || hrefEmpty {
+ return errors.Errorf("branding %s and %s must both be set or both empty", labelField, hrefField)
+ }
+ if err := validateFooterNavHref(hrefField, href); err != nil {
+ return err
+ }
+ return nil
+}
+
+func validateFooterNav(f *BrandingFooter) error {
+ pairs := []struct{ labelField, hrefField, label, href string }{
+ {"footer.created_by_label", "footer.created_by_href", f.CreatedByLabel, f.CreatedByHref},
+ {"footer.brand_label", "footer.brand_href", f.BrandLabel, f.BrandHref},
+ {"footer.suite_label", "footer.suite_href", f.SuiteLabel, f.SuiteHref},
+ {"footer.support_label", "footer.support_href", f.SupportLabel, f.SupportHref},
+ {"footer.send_tx_label", "footer.send_tx_href", f.SendTxLabel, f.SendTxHref},
+ {"footer.promo_label", "footer.promo_href", f.PromoLabel, f.PromoHref},
+ }
+ for _, p := range pairs {
+ if err := validateOptionalFooterPair(p.labelField, p.hrefField, p.label, p.href); err != nil {
+ return err
+ }
+ }
+ // Terms row uses branding.tos_link; empty terms_label hides the link in templates.
+ return nil
+}
diff --git a/common/branding_test.go b/common/branding_test.go
new file mode 100644
index 0000000000..b31f32ee72
--- /dev/null
+++ b/common/branding_test.go
@@ -0,0 +1,228 @@
+package common
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+// trezorFooterJSON returns a full Trezor-style footer object for test fixtures.
+func trezorFooterJSON() map[string]interface{} {
+ return map[string]interface{}{
+ "created_by_label": "Created by SatoshiLabs",
+ "created_by_href": "https://satoshilabs.com/",
+ "terms_label": "Terms of Use",
+ "brand_label": "Trezor",
+ "brand_href": "https://trezor.io/",
+ "suite_label": "Suite",
+ "suite_href": "https://trezor.io/trezor-suite",
+ "support_label": "Support",
+ "support_href": "https://trezor.io/support",
+ "send_tx_label": "Send Transaction",
+ "send_tx_href": "/sendtx",
+ "promo_label": "Don't have a Trezor? Get one!",
+ "promo_href": "https://trezor.io/compare",
+ }
+}
+
+func mergeBranding(br map[string]interface{}) map[string]interface{} {
+ br["footer"] = trezorFooterJSON()
+ return br
+}
+
+func TestLoadBrandingDefaults(t *testing.T) {
+ dir := t.TempDir()
+ br := mergeBranding(map[string]interface{}{
+ "brand_name": "Trezor",
+ "about": "Blockbook - blockchain indexer for Trezor Suite https://trezor.io/trezor-suite. Do not use for any other purpose.",
+ "tos_link": "https://trezor.io/terms-of-use",
+ "github_repo_url": "https://github.com/trezor/blockbook",
+ "fiat_rates_credit": "Exchange rates provided by Coingecko.",
+ "logo_url": "/static/img/logo.svg",
+ "favicon_url": "/static/favicon.ico",
+ "logo_width_px": 128,
+ "logo_right_padding_px": 140,
+ })
+ writeJSON(t, filepath.Join(dir, "environ.json"), map[string]interface{}{
+ "version": "0.6.0",
+ "branding": br,
+ })
+ b, err := LoadBranding(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if b.BrandName != "Trezor" {
+ t.Fatalf("BrandName = %q", b.BrandName)
+ }
+ if b.TOSLink != "https://trezor.io/terms-of-use" {
+ t.Fatalf("TOSLink = %q", b.TOSLink)
+ }
+ if b.Footer.TermsLabel != "Terms of Use" {
+ t.Fatalf("Footer.TermsLabel = %q", b.Footer.TermsLabel)
+ }
+}
+
+func TestLoadBrandingOverride(t *testing.T) {
+ dir := t.TempDir()
+ br := mergeBranding(map[string]interface{}{
+ "brand_name": "Trezor",
+ "about": "about-default",
+ "tos_link": "https://trezor.io/terms-of-use",
+ "github_repo_url": "https://github.com/trezor/blockbook",
+ "fiat_rates_credit": "credit-default",
+ "logo_url": "/static/img/logo.svg",
+ "favicon_url": "/static/favicon.ico",
+ "logo_width_px": 128,
+ "logo_right_padding_px": 140,
+ })
+ writeJSON(t, filepath.Join(dir, "environ.json"), map[string]interface{}{
+ "version": "0.6.0",
+ "branding": br,
+ })
+ override := map[string]interface{}{
+ "branding": map[string]interface{}{
+ "brand_name": "Edge",
+ "about": "about-override",
+ "fiat_rates_credit": "",
+ },
+ }
+ writeJSON(t, filepath.Join(dir, "environ.overrides.json"), override)
+
+ b, err := LoadBranding(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if b.BrandName != "Edge" {
+ t.Fatalf("BrandName = %q", b.BrandName)
+ }
+ if b.About != "about-override" {
+ t.Fatalf("About = %q", b.About)
+ }
+ if b.FiatRatesCredit != "" {
+ t.Fatalf("FiatRatesCredit = %q", b.FiatRatesCredit)
+ }
+ if b.TOSLink != "https://trezor.io/terms-of-use" {
+ t.Fatalf("TOSLink should remain from base: %q", b.TOSLink)
+ }
+}
+
+func TestLoadBrandingMissingFile(t *testing.T) {
+ _, err := LoadBranding(filepath.Join(t.TempDir(), "nonexistent-subdir"))
+ if err == nil {
+ t.Fatal("expected error")
+ }
+}
+
+func TestLoadBrandingEmptyAbout(t *testing.T) {
+ dir := t.TempDir()
+ br := mergeBranding(map[string]interface{}{
+ "brand_name": "X",
+ "about": " ",
+ "tos_link": "https://trezor.io/terms-of-use",
+ "github_repo_url": "https://github.com/trezor/blockbook",
+ "fiat_rates_credit": "",
+ "logo_url": "/x",
+ "favicon_url": "/f",
+ "logo_width_px": 1,
+ "logo_right_padding_px": 1,
+ })
+ writeJSON(t, filepath.Join(dir, "environ.json"), map[string]interface{}{
+ "branding": br,
+ })
+ _, err := LoadBranding(dir)
+ if err == nil {
+ t.Fatal("expected error for empty about")
+ }
+}
+
+func TestLoadBrandingOptionalFooterSlots(t *testing.T) {
+ dir := t.TempDir()
+ foot := trezorFooterJSON()
+ foot["suite_label"] = ""
+ foot["suite_href"] = ""
+ foot["promo_label"] = ""
+ foot["promo_href"] = ""
+ br := mergeBranding(map[string]interface{}{
+ "brand_name": "X",
+ "about": "y",
+ "tos_link": "https://trezor.io/terms-of-use",
+ "github_repo_url": "https://github.com/trezor/blockbook",
+ "fiat_rates_credit": "",
+ "logo_url": "/x",
+ "favicon_url": "/f",
+ "logo_width_px": 1,
+ "logo_right_padding_px": 1,
+ })
+ br["footer"] = foot
+ writeJSON(t, filepath.Join(dir, "environ.json"), map[string]interface{}{
+ "version": "0.6.0",
+ "branding": br,
+ })
+ b, err := LoadBranding(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if b.Footer.SuiteLabel != "" || b.Footer.PromoLabel != "" {
+ t.Fatalf("expected empty suite/promo labels, got suite=%q promo=%q", b.Footer.SuiteLabel, b.Footer.PromoLabel)
+ }
+}
+
+func TestLoadBrandingFooterPartialPairError(t *testing.T) {
+ dir := t.TempDir()
+ foot := trezorFooterJSON()
+ foot["suite_href"] = ""
+ br := mergeBranding(map[string]interface{}{
+ "brand_name": "X",
+ "about": "y",
+ "tos_link": "https://trezor.io/terms-of-use",
+ "github_repo_url": "https://github.com/trezor/blockbook",
+ "fiat_rates_credit": "",
+ "logo_url": "/x",
+ "favicon_url": "/f",
+ "logo_width_px": 1,
+ "logo_right_padding_px": 1,
+ })
+ br["footer"] = foot
+ writeJSON(t, filepath.Join(dir, "environ.json"), map[string]interface{}{
+ "version": "0.6.0",
+ "branding": br,
+ })
+ _, err := LoadBranding(dir)
+ if err == nil {
+ t.Fatal("expected error when suite_label is set but suite_href is empty")
+ }
+}
+
+func TestLoadBrandingInvalidTOS(t *testing.T) {
+ dir := t.TempDir()
+ br := mergeBranding(map[string]interface{}{
+ "brand_name": "X",
+ "about": "y",
+ "tos_link": "not-a-url",
+ "github_repo_url": "https://github.com/trezor/blockbook",
+ "fiat_rates_credit": "",
+ "logo_url": "/x",
+ "favicon_url": "/f",
+ "logo_width_px": 1,
+ "logo_right_padding_px": 1,
+ })
+ writeJSON(t, filepath.Join(dir, "environ.json"), map[string]interface{}{
+ "branding": br,
+ })
+ _, err := LoadBranding(dir)
+ if err == nil {
+ t.Fatal("expected error for invalid tos_link")
+ }
+}
+
+func writeJSON(t *testing.T, path string, v interface{}) {
+ t.Helper()
+ b, err := json.Marshal(v)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, b, 0o644); err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/configs/environ.json b/configs/environ.json
index b1e38d6012..3cdb51bab9 100644
--- a/configs/environ.json
+++ b/configs/environ.json
@@ -3,5 +3,31 @@
"backend_install_path": "/opt/coins/nodes",
"backend_data_path": "/opt/coins/data",
"blockbook_install_path": "/opt/coins/blockbook",
- "blockbook_data_path": "/opt/coins/data"
+ "blockbook_data_path": "/opt/coins/data",
+ "branding": {
+ "brand_name": "Trezor",
+ "about": "Blockbook - blockchain indexer for Trezor Suite https://trezor.io/trezor-suite. Do not use for any other purpose.",
+ "tos_link": "https://trezor.io/terms-of-use",
+ "github_repo_url": "https://github.com/trezor/blockbook",
+ "fiat_rates_credit": "Exchange rates provided by Coingecko.",
+ "logo_url": "/static/img/logo.svg",
+ "logo_width_px": 128,
+ "logo_right_padding_px": 140,
+ "favicon_url": "/static/favicon.ico",
+ "footer": {
+ "created_by_label": "Created by SatoshiLabs",
+ "created_by_href": "https://satoshilabs.com/",
+ "terms_label": "Terms of Use",
+ "brand_label": "Trezor",
+ "brand_href": "https://trezor.io/",
+ "suite_label": "Suite",
+ "suite_href": "https://trezor.io/trezor-suite",
+ "support_label": "Support",
+ "support_href": "https://trezor.io/support",
+ "send_tx_label": "Send Transaction",
+ "send_tx_href": "/sendtx",
+ "promo_label": "Don't have a Trezor? Get one!",
+ "promo_href": "https://trezor.io/compare"
+ }
+ }
}
diff --git a/docs/build.md b/docs/build.md
index 4eeadd5b8f..e5e1ffbc66 100644
--- a/docs/build.md
+++ b/docs/build.md
@@ -127,6 +127,16 @@ Both Blockbook and back-end have separated install and data directories. They us
*coin* used above is defined in *coin.alias* in coin definition file.
+**explorer branding**
+
+The public explorer UI and API `about` / Terms-of-Use link are driven by the `branding` object in *configs/environ.json*
+(committed defaults). To customize without editing tracked files, copy that `branding` object into
+*configs/environ.overrides.json* (gitignored) and adjust fields such as `brand_name`, `about`, `tos_link`,
+`github_repo_url`, `logo_url`, `logo_width_px`, `logo_right_padding_px`, `favicon_url`, `fiat_rates_credit`, and the
+nested `footer` object (label/href fields under *configs/environ.json* `branding.footer`). Explorer footer **layout**
+(order and Bootstrap classes) is fixed in *static/templates/base.html*; **copy** and hrefs come from `footer` except
+Terms, which uses `tos_link`. Default logo is *static/img/logo.svg*; replace that file or point `logo_url` elsewhere.
+
**package names**
Package names are defined in *backend.package_name* and *blockbook.package_name* in coin definition file. We use
diff --git a/docs/config.md b/docs/config.md
index 70e52e1325..640277aeb1 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -124,10 +124,8 @@ Backend templates may also reference `.Env.RPCBindHost` and `.Env.RPCAllowIP`, w
## Built-in text
Since Blockbook is an open-source project and we don't prevent anybody from running independent instances, it is possible
-to alter built-in text that is specific for Trezor. Text fields that could be updated are:
-
- * [about](/build/text/about) – A note about instance shown on the Application status page and returned by an API.
- * [tos_link](/build/text/tos_link) – A link to Terms of service shown as the footer on the Explorer pages.
-
-Text data are stored as plain text files in *build/text* directory and are embedded to binary during build. A change of
-these files is meant for a private purpose and PRs that would update them won't be accepted.
+to alter branding and static copy (including Trezor-specific defaults). The `about` string and `tos_link` URL are part of
+the `branding` object in *configs/environ.json*, with optional overrides in *configs/environ.overrides.json* (see
+*docs/build.md*, section *explorer branding*). `about` is shown on the Application status page and returned by the API;
+`tos_link` is the Terms-of-Use URL; `branding.footer` supplies the rest of the explorer footer labels and hrefs (layout
+is fixed in templates).
diff --git a/server/public.go b/server/public.go
index ab85fafb27..3fe7229107 100644
--- a/server/public.go
+++ b/server/public.go
@@ -75,11 +75,15 @@ type PublicServer struct {
fiatRates *fiat.FiatRates
useSatsAmountFormat bool
isFullInterface bool
+ branding *common.Branding
}
// NewPublicServer creates new public server http interface to blockbook and returns its handle
// only basic functionality is mapped, to map all functions, call
-func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, explorerURL string, metrics *common.Metrics, is *common.InternalState, fiatRates *fiat.FiatRates, debugMode bool) (*PublicServer, error) {
+func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, explorerURL string, metrics *common.Metrics, is *common.InternalState, fiatRates *fiat.FiatRates, debugMode bool, branding *common.Branding) (*PublicServer, error) {
+ if branding == nil {
+ return nil, fmt.Errorf("branding is required")
+ }
api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is, fiatRates)
if err != nil {
@@ -124,6 +128,7 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch
is: is,
fiatRates: fiatRates,
useSatsAmountFormat: chain.GetChainParser().GetChainType() == bchain.ChainBitcoinType && chain.GetChainParser().AmountDecimals() == 8,
+ branding: branding,
}
s.htmlTemplates.newTemplateData = s.newTemplateData
s.htmlTemplates.newTemplateDataWithError = s.newTemplateDataWithError
@@ -318,7 +323,7 @@ func (s *PublicServer) newTemplateData(r *http.Request) *TemplateData {
CoinLabel: s.is.CoinLabel,
ChainType: s.chainParser.GetChainType(),
InternalExplorer: s.internalExplorer && !s.is.InitialSync,
- TOSLink: api.Text.TOSLink,
+ Branding: s.branding,
}
if t.ChainType == bchain.ChainEthereumType {
t.FungibleTokenName = bchain.EthereumTokenStandardMap[bchain.FungibleToken]
@@ -417,7 +422,7 @@ type TemplateData struct {
PagingRange []int
PageParams template.URL
Minified string
- TOSLink string
+ Branding *common.Branding
SendTxHex string
Status string
NonZeroBalanceTokens bool
diff --git a/server/public_ethereumtype_test.go b/server/public_ethereumtype_test.go
index bd21cc3998..2f5da304a8 100644
--- a/server/public_ethereumtype_test.go
+++ b/server/public_ethereumtype_test.go
@@ -27,7 +27,7 @@ func httpTestsEthereumType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `
Trezor Fake Coin ExplorerAddress address7b.eth
0x7B62EB7fe80350DC7EC945C0B73242cb9877FB1b
0.000000000123450123 FAKE
0.00 USD
Confirmed | |
| Balance | 0.000000000123450123 FAKE0.00 USD |
| Transactions | 2 |
| Non-contract Transactions | 0 |
| Internal Transactions | 0 |
| Nonce | 123 |
Transactions
ERC20 Token Transfers
7.675000000000000001 S13-
7.674999999999991915 S13-
`,
+ `Trezor Fake Coin ExplorerAddress address7b.eth
0x7B62EB7fe80350DC7EC945C0B73242cb9877FB1b
0.000000000123450123 FAKE
0.00 USD
Confirmed | |
| Balance | 0.000000000123450123 FAKE0.00 USD |
| Transactions | 2 |
| Non-contract Transactions | 0 |
| Internal Transactions | 0 |
| Nonce | 123 |
Transactions
ERC20 Token Transfers
7.675000000000000001 S13-
7.674999999999991915 S13-
`,
},
},
{
@@ -36,7 +36,7 @@ func httpTestsEthereumType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `Trezor Fake Coin ExplorerAddress
0x5Dc6288b35E0807A3d6fEB89b3a2Ff4aB773168e
0.000000000123450093 FAKE
0.00 USD
Confirmed | |
| Balance | 0.000000000123450093 FAKE0.00 USD |
| Transactions | 1 |
| Non-contract Transactions | 1 |
| Internal Transactions | 0 |
| Nonce | 93 |
Transactions
0x5Dc6288b35E0807A3d6fEB89b3a2Ff4aB773168e
0 FAKE0.00 USD0.00 USD
ERC1155 Token Transfers
0x5Dc6288b35E0807A3d6fEB89b3a2Ff4aB773168e
`,
+ `Trezor Fake Coin ExplorerAddress
0x5Dc6288b35E0807A3d6fEB89b3a2Ff4aB773168e
0.000000000123450093 FAKE
0.00 USD
Confirmed | |
| Balance | 0.000000000123450093 FAKE0.00 USD |
| Transactions | 1 |
| Non-contract Transactions | 1 |
| Internal Transactions | 0 |
| Nonce | 93 |
Transactions
0x5Dc6288b35E0807A3d6fEB89b3a2Ff4aB773168e
0 FAKE0.00 USD0.00 USD
ERC1155 Token Transfers
0x5Dc6288b35E0807A3d6fEB89b3a2Ff4aB773168e
`,
},
},
{
@@ -45,14 +45,14 @@ func httpTestsEthereumType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `Trezor Fake Coin ExplorerTransaction
0xa9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b101
| In Block | Unconfirmed |
| Status | Success |
| Value | 0 FAKE0.00 USD0.00 USD |
| Gas Used / Limit | 52025 / 78037 |
| Gas Price | 0.00000004 FAKE0.00 USD0.00 USD (40 Gwei) |
| Max Priority Fee Per Gas | 0.000000040000000001 FAKE0.00 USD0.00 USD (40.000000001 Gwei) |
| Max Fee Per Gas | 0.000000040000000002 FAKE0.00 USD0.00 USD (40.000000002 Gwei) |
| Base Fee Per Gas | 0.000000040000000003 FAKE0.00 USD0.00 USD (40.000000003 Gwei) |
| Fees | 0.002081 FAKE4.16 USD18.55 USD |
| RBF | ON |
| Nonce | 208 |
`,
+ `Trezor Fake Coin ExplorerTransaction
0xa9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b101
| In Block | Unconfirmed |
| Status | Success |
| Value | 0 FAKE0.00 USD0.00 USD |
| Gas Used / Limit | 52025 / 78037 |
| Gas Price | 0.00000004 FAKE0.00 USD0.00 USD (40 Gwei) |
| Max Priority Fee Per Gas | 0.000000040000000001 FAKE0.00 USD0.00 USD (40.000000001 Gwei) |
| Max Fee Per Gas | 0.000000040000000002 FAKE0.00 USD0.00 USD (40.000000002 Gwei) |
| Base Fee Per Gas | 0.000000040000000003 FAKE0.00 USD0.00 USD (40.000000003 Gwei) |
| Fees | 0.002081 FAKE4.16 USD18.55 USD |
| RBF | ON |
| Nonce | 208 |
`,
},
}, {
name: "explorerTokenDetail " + dbtestdata.EthAddr7b,
r: newGetRequest(ts.URL + "/nft/" + dbtestdata.EthAddrContractCd + "/" + "1"),
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
- body: []string{`Trezor Fake Coin Explorer`},
+ body: []string{`Trezor Fake Coin Explorer`},
},
{
name: "apiIndex",
diff --git a/server/public_test.go b/server/public_test.go
index 12d3bf77b2..74335e4b34 100644
--- a/server/public_test.go
+++ b/server/public_test.go
@@ -23,6 +23,7 @@ import (
"github.com/martinboehm/btcutil/chaincfg"
gosocketio "github.com/martinboehm/golang-socketio"
"github.com/martinboehm/golang-socketio/transport"
+ "github.com/trezor/blockbook/api"
"github.com/trezor/blockbook/bchain"
"github.com/trezor/blockbook/bchain/coins/btc"
"github.com/trezor/blockbook/common"
@@ -31,11 +32,19 @@ import (
"github.com/trezor/blockbook/tests/dbtestdata"
)
+var testBranding *common.Branding
+
func TestMain(m *testing.M) {
// set the current directory to blockbook root so that ./static/ works
if err := os.Chdir(".."); err != nil {
glog.Fatal("Chdir error:", err)
}
+ var err error
+ testBranding, err = common.LoadBranding("configs")
+ if err != nil {
+ glog.Fatal("LoadBranding:", err)
+ }
+ api.SetBranding(testBranding.About, testBranding.TOSLink)
c := m.Run()
chaincfg.ResetParams()
os.Exit(c)
@@ -140,7 +149,7 @@ func setupPublicHTTPServer(parser bchain.BlockChainParser, chain bchain.BlockCha
}
// s.Run is never called, binding can be to any port
- s, err := NewPublicServer("localhost:12345", "", d, chain, mempool, txCache, "", metrics, is, fiatRates, false)
+ s, err := NewPublicServer("localhost:12345", "", d, chain, mempool, txCache, "", metrics, is, fiatRates, false, testBranding)
if err != nil {
t.Fatal(err)
}
@@ -390,7 +399,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `Trezor Fake Coin ExplorerTransaction
fdd824a780cbb718eeb766eb05d83fdefc793a27082cd5e67f856d69798cf7db
| Mined Time | 1639 days 11 hours ago |
| In Block | 00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6 |
| In Block Height | 225494 |
| Total Input | 0 FAKE |
| Total Output | 13.60030331 FAKE |
| Fees | 0 FAKE |
mined 1639 days 11 hours ago
No Inputs (Newly Generated Coins)
`,
+ `Trezor Fake Coin ExplorerTransaction
fdd824a780cbb718eeb766eb05d83fdefc793a27082cd5e67f856d69798cf7db
| Mined Time | 1639 days 11 hours ago |
| In Block | 00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6 |
| In Block Height | 225494 |
| Total Input | 0 FAKE |
| Total Output | 13.60030331 FAKE |
| Fees | 0 FAKE |
mined 1639 days 11 hours ago
No Inputs (Newly Generated Coins)
`,
},
},
{
@@ -399,7 +408,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `Trezor Fake Coin ExplorerAddress
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz
0.00012345 FAKE
Confirmed | |
| Total Received | 0.00024690 FAKE |
| Total Sent | 0.00012345 FAKE |
| Final Balance | 0.00012345 FAKE |
| No. Transactions | 2 |
Transactions
mined 1639 days 11 hours ago
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE
OP_RETURN 2020f1686f6a200 FAKE×
mined 1640 days 9 hours ago
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE→ mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE×
`,
+ `Trezor Fake Coin ExplorerAddress
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz
0.00012345 FAKE
Confirmed | |
| Total Received | 0.00024690 FAKE |
| Total Sent | 0.00012345 FAKE |
| Final Balance | 0.00012345 FAKE |
| No. Transactions | 2 |
Transactions
mined 1639 days 11 hours ago
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE
OP_RETURN 2020f1686f6a200 FAKE×
mined 1640 days 9 hours ago
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE→ mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE×
`,
},
},
{
@@ -408,7 +417,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `Trezor Fake Coin ExplorerTransaction
3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71
| Mined Time | 1639 days 11 hours ago |
| In Block | 00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6 |
| In Block Height | 225494 |
| Total Input | 3172.83951062 FAKE |
| Total Output | 3172.83951000 FAKE |
| Fees | 0.00000062 FAKE |
mined 1639 days 11 hours ago
`,
+ `Trezor Fake Coin ExplorerTransaction
3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71
| Mined Time | 1639 days 11 hours ago |
| In Block | 00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6 |
| In Block Height | 225494 |
| Total Input | 3172.83951062 FAKE |
| Total Output | 3172.83951000 FAKE |
| Fees | 0.00000062 FAKE |
mined 1639 days 11 hours ago
`,
},
},
{
@@ -417,7 +426,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `Trezor Fake Coin ExplorerError
Transaction not found
`,
+ `Trezor Fake Coin ExplorerError
Transaction not found
`,
},
},
{
@@ -426,7 +435,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `Trezor Fake Coin ExplorerBlocks
| Height | Hash | Timestamp | Transactions | Size |
|---|
| 225494 | 00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6 | 1639 days 11 hours ago | 4 | 2345678 |
| 225493 | 0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997 | 1640 days 9 hours ago | 2 | 1234567 |
`,
+ `
Trezor Fake Coin ExplorerBlocks
| Height | Hash | Timestamp | Transactions | Size |
|---|
| 225494 | 00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6 | 1639 days 11 hours ago | 4 | 2345678 |
| 225493 | 0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997 | 1640 days 9 hours ago | 2 | 1234567 |
`,
},
},
{
@@ -435,7 +444,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `
Trezor Fake Coin Explorer225494
00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
| Transactions | 4 |
| Height | 225494 |
| Confirmations | 1 |
| Timestamp | 1639 days 11 hours ago |
| Size (bytes) | 2345678 |
| Version | |
| Merkle Root | |
| Nonce | |
| Bits | |
| Difficulty | |
mined 1639 days 11 hours ago
OP_RETURN 2020f1686f6a200 FAKE×
mined 1639 days 11 hours ago
mined 1639 days 11 hours ago
mined 1639 days 11 hours ago
No Inputs (Newly Generated Coins)
`,
+ `
Trezor Fake Coin Explorer225494
00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
| Transactions | 4 |
| Height | 225494 |
| Confirmations | 1 |
| Timestamp | 1639 days 11 hours ago |
| Size (bytes) | 2345678 |
| Version | |
| Merkle Root | |
| Nonce | |
| Bits | |
| Difficulty | |
mined 1639 days 11 hours ago
OP_RETURN 2020f1686f6a200 FAKE×
mined 1639 days 11 hours ago
mined 1639 days 11 hours ago
mined 1639 days 11 hours ago
No Inputs (Newly Generated Coins)
`,
},
},
{
@@ -444,9 +453,9 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `
Trezor Fake Coin ExplorerApplication status
Synchronization with backend is disabled, the state of index is not up to date.
Blockbook | |
| Coin | Fakecoin |
| Host | |
| Version / Commit / Build | unknown / unknown / unknown |
| Synchronized | true |
| Last Block | 225494 |
| Last Block Update | `,
+ `Trezor Fake Coin ExplorerApplication statusSynchronization with backend is disabled, the state of index is not up to date.Blockbook | | | Coin | Fakecoin | | Host | | | Version / Commit / Build | unknown / unknown / unknown | | Synchronized | true | | Last Block | 225494 | | Last Block Update | `,
` | | Mempool in Sync | false | | Last Mempool Update | | | Transactions in Mempool | 0 | | Current Fiat rates | `,
- `
Backend | | | Chain | fakecoin | | Version | 001001 | | Subversion | /Fakecoin:0.0.1/ | | Last Block | 2 | | Difficulty | |
Blockbook - blockchain indexer for Trezor Suite https://trezor.io/trezor-suite. Do not use for any other purpose. `,
+ ` |
Backend | |
| Chain | fakecoin |
| Version | 001001 |
| Subversion | /Fakecoin:0.0.1/ |
| Last Block | 2 |
| Difficulty | |
Blockbook - blockchain indexer for Trezor Suite https://trezor.io/trezor-suite. Do not use for any other purpose.`,
},
},
{
@@ -455,7 +464,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `
Trezor Fake Coin Explorer225494
00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
| Transactions | 4 |
| Height | 225494 |
| Confirmations | 1 |
| Timestamp | 1639 days 11 hours ago |
| Size (bytes) | 2345678 |
| Version | |
| Merkle Root | |
| Nonce | |
| Bits | |
| Difficulty | |
mined 1639 days 11 hours ago
OP_RETURN 2020f1686f6a200 FAKE×
mined 1639 days 11 hours ago
mined 1639 days 11 hours ago
mined 1639 days 11 hours ago
No Inputs (Newly Generated Coins)
`,
+ `
Trezor Fake Coin Explorer225494
00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
| Transactions | 4 |
| Height | 225494 |
| Confirmations | 1 |
| Timestamp | 1639 days 11 hours ago |
| Size (bytes) | 2345678 |
| Version | |
| Merkle Root | |
| Nonce | |
| Bits | |
| Difficulty | |
mined 1639 days 11 hours ago
OP_RETURN 2020f1686f6a200 FAKE×
mined 1639 days 11 hours ago
mined 1639 days 11 hours ago
mined 1639 days 11 hours ago
No Inputs (Newly Generated Coins)
`,
},
},
{
@@ -464,7 +473,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `
Trezor Fake Coin Explorer225494
00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
| Transactions | 4 |
| Height | 225494 |
| Confirmations | 1 |
| Timestamp | 1639 days 11 hours ago |
| Size (bytes) | 2345678 |
| Version | |
| Merkle Root | |
| Nonce | |
| Bits | |
| Difficulty | |
mined 1639 days 11 hours ago
OP_RETURN 2020f1686f6a200 FAKE×
mined 1639 days 11 hours ago
mined 1639 days 11 hours ago
mined 1639 days 11 hours ago
No Inputs (Newly Generated Coins)
`,
+ `
Trezor Fake Coin Explorer225494
00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
| Transactions | 4 |
| Height | 225494 |
| Confirmations | 1 |
| Timestamp | 1639 days 11 hours ago |
| Size (bytes) | 2345678 |
| Version | |
| Merkle Root | |
| Nonce | |
| Bits | |
| Difficulty | |
mined 1639 days 11 hours ago
OP_RETURN 2020f1686f6a200 FAKE×
mined 1639 days 11 hours ago
mined 1639 days 11 hours ago
mined 1639 days 11 hours ago
No Inputs (Newly Generated Coins)
`,
},
},
{
@@ -473,7 +482,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `
Trezor Fake Coin ExplorerTransaction
fdd824a780cbb718eeb766eb05d83fdefc793a27082cd5e67f856d69798cf7db
| Mined Time | 1639 days 11 hours ago |
| In Block | 00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6 |
| In Block Height | 225494 |
| Total Input | 0 FAKE |
| Total Output | 13.60030331 FAKE |
| Fees | 0 FAKE |
mined 1639 days 11 hours ago
No Inputs (Newly Generated Coins)
`,
+ `
Trezor Fake Coin ExplorerTransaction
fdd824a780cbb718eeb766eb05d83fdefc793a27082cd5e67f856d69798cf7db
| Mined Time | 1639 days 11 hours ago |
| In Block | 00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6 |
| In Block Height | 225494 |
| Total Input | 0 FAKE |
| Total Output | 13.60030331 FAKE |
| Fees | 0 FAKE |
mined 1639 days 11 hours ago
No Inputs (Newly Generated Coins)
`,
},
},
{
@@ -482,7 +491,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `
Trezor Fake Coin ExplorerAddress
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz
0.00012345 FAKE
Confirmed | |
| Total Received | 0.00024690 FAKE |
| Total Sent | 0.00012345 FAKE |
| Final Balance | 0.00012345 FAKE |
| No. Transactions | 2 |
Transactions
mined 1639 days 11 hours ago
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE
OP_RETURN 2020f1686f6a200 FAKE×
mined 1640 days 9 hours ago
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE→ mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE×
`,
+ `
Trezor Fake Coin ExplorerAddress
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz
0.00012345 FAKE
Confirmed | |
| Total Received | 0.00024690 FAKE |
| Total Sent | 0.00012345 FAKE |
| Final Balance | 0.00012345 FAKE |
| No. Transactions | 2 |
Transactions
mined 1639 days 11 hours ago
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE
OP_RETURN 2020f1686f6a200 FAKE×
mined 1640 days 9 hours ago
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE→ mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE×
`,
},
},
{
@@ -491,7 +500,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `
Trezor Fake Coin ExplorerXPUB
upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q
1186.419755 FAKE
Confirmed | |
| Total Received | 1186.41975501 FAKE |
| Total Sent | 0.00000001 FAKE |
| Final Balance | 1186.41975500 FAKE |
| No. Transactions | 2 |
| Used XPUB Addresses | 2 |
Transactions
mined 1639 days 11 hours ago
mined 1640 days 9 hours ago
`,
+ `
Trezor Fake Coin ExplorerXPUB
upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q
1186.419755 FAKE
Confirmed | |
| Total Received | 1186.41975501 FAKE |
| Total Sent | 0.00000001 FAKE |
| Final Balance | 1186.41975500 FAKE |
| No. Transactions | 2 |
| Used XPUB Addresses | 2 |
Transactions
mined 1639 days 11 hours ago
mined 1640 days 9 hours ago
`,
},
},
{
@@ -500,7 +509,12 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `
Trezor Fake Coin ExplorerXPUB
tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{0,1}/*)#4rqwxvej
0 FAKE
Confirmed | |
| Total Received | 0 FAKE |
| Total Sent | 0 FAKE |
| Final Balance | 0 FAKE |
| No. Transactions | 0 |
| Used XPUB Addresses | 0 |
XPUB Addresses with Balance | |
| No addresses |
`,
+ `
Trezor Fake Coin Explorer`,
+ `
XPUB
`,
+ `tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{0,1}/*)#4rqwxvej`,
+ `new QRCode(document.getElementById("qrcode"), { text: "tr([5c9e228d`,
+ `#4rqwxvej", width: 120, height: 120 });`,
+ `Created by SatoshiLabs`,
},
},
{
@@ -509,7 +523,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `
Trezor Fake Coin ExplorerError
No matching records found for '1234'
`,
+ `
Trezor Fake Coin ExplorerError
No matching records found for '1234'
`,
},
},
{
@@ -518,7 +532,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `
Trezor Fake Coin Explorer`,
+ `
Trezor Fake Coin ExplorerSend Raw Transaction
`,
},
},
{
@@ -527,7 +541,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "text/html; charset=utf-8",
body: []string{
- `
Trezor Fake Coin ExplorerSend Raw Transaction
Invalid data
`,
+ `
Trezor Fake Coin ExplorerSend Raw Transaction
Invalid data
`,
},
},
{
diff --git a/static/css/main.css b/static/css/main.css
index e708ac188c..2eda52a6f0 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -340,12 +340,11 @@ span.btn-paging:hover {
}
.trezor-logo {
- width: 128px;
height: 32px;
position: absolute;
top: 16px;
- background-size: cover;
- background-image: url("data:image/svg+xml,%3Csvg style='width: 128px%3B' version='1.1' xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 163.7 41.9' space='preserve'%3E%3Cpolygon points='101.1 12.8 118.2 12.8 118.2 17.3 108.9 29.9 118.2 29.9 118.2 35.2 101.1 35.2 101.1 30.7 110.4 18.1 101.1 18.1'%3E%3C/polygon%3E%3Cpath d='M158.8 26.9c2.1-0.8 4.3-2.9 4.3-6.6c0-4.5-3.1-7.4-7.7-7.4h-10.5v22.3h5.8v-7.5h2.2l4.1 7.5h6.7L158.8 26.9z M154.7 22.5h-4V18h4c1.5 0 2.5 0.9 2.5 2.2C157.2 21.6 156.2 22.5 154.7 22.5z'%3E%3C/path%3E%3Cpath d='M130.8 12.5c-6.8 0-11.6 4.9-11.6 11.5s4.9 11.5 11.6 11.5s11.7-4.9 11.7-11.5S137.6 12.5 130.8 12.5z M130.8 30.3c-3.4 0-5.7-2.6-5.7-6.3c0-3.8 2.3-6.3 5.7-6.3c3.4 0 5.8 2.6 5.8 6.3C136.6 27.7 134.2 30.3 130.8 30.3z'%3E%3C/path%3E%3Cpolygon points='82.1 12.8 98.3 12.8 98.3 18 87.9 18 87.9 21.3 98 21.3 98 26.4 87.9 26.4 87.9 30 98.3 30 98.3 35.2 82.1 35.2'%3E%3C/polygon%3E%3Cpath d='M24.6 9.7C24.6 4.4 20 0 14.4 0S4.2 4.4 4.2 9.7v3.1H0v22.3h0l14.4 6.7l14.4-6.7h0V12.9h-4.2V9.7z M9.4 9.7c0-2.5 2.2-4.5 5-4.5s5 2 5 4.5v3.1H9.4V9.7z M23 31.5l-8.6 4l-8.6-4V18.1H23V31.5z'%3E%3C/path%3E%3Cpath d='M79.4 20.3c0-4.5-3.1-7.4-7.7-7.4H61.2v22.3H67v-7.5h2.2l4.1 7.5H80l-4.9-8.3C77.2 26.1 79.4 24 79.4 20.3z M71 22.5h-4V18h4c1.5 0 2.5 0.9 2.5 2.2C73.5 21.6 72.5 22.5 71 22.5z'%3E%3C/path%3E%3Cpolygon points='40.5 12.8 58.6 12.8 58.6 18.1 52.4 18.1 52.4 35.2 46.6 35.2 46.6 18.1 40.5 18.1'%3E%3C/polygon%3E%3C/svg%3E");
+ background-size: contain;
+ background-repeat: no-repeat;
}
.copyable::before,
diff --git a/static/css/main.min.4.css b/static/css/main.min.4.css
index 54dd88a23d..4bd35fc9ba 100644
--- a/static/css/main.min.4.css
+++ b/static/css/main.min.4.css
@@ -1 +1 @@
-@import "TTHoves/TTHoves.css";* {margin: 0;padding: 0;outline: none;font-family: "TT Hoves", -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif;}html, body {height: 100%;}body {min-height: 100%;margin: 0;background: linear-gradient(to bottom, #f6f6f6 360px, #e5e5e5 0), #e5e5e5;background-repeat: no-repeat;}a {color: #00854d;text-decoration: none;}a:hover {color: #00854d;text-decoration: underline;}select {border-radius: 0.5rem;padding-left: 0.5rem;border: 1px solid #ced4da;color: var(--bs-body-color);min-height: 45px;}#header {position: fixed;top: 0;left: 0;width: 100%;margin: 0;padding-bottom: 0;padding-top: 0;background-color: white;border-bottom: 1px solid #f6f6f6;z-index: 10;}#header a {color: var(--bs-navbar-brand-color);}#header a:hover {color: var(--bs-navbar-brand-hover-color);}#header .navbar {--bs-navbar-padding-y: 0.7rem;}#header .form-control-lg {font-size: 1rem;padding: 0.75rem 1rem;}#header .container {min-height: 50px;}#header .btn.dropdown-toggle {padding-right: 0;}#header .dropdown-menu {--bs-dropdown-min-width: 13rem;}#header .dropdown-menu[data-bs-popper] {left: initial;right: 0;}#header .dropdown-menu.show {display: flex;}.form-control:focus {outline: 0;box-shadow: none;border-color: #00854d;}.base-value {color: #757575 !important;padding-left: 0.5rem;font-weight: normal;}.badge {vertical-align: middle;text-transform: uppercase;letter-spacing: 0.15em;--bs-badge-padding-x: 0.8rem;--bs-badge-font-weight: normal;--bs-badge-border-radius: 0.6rem;}.bg-secondary {background-color: #757575 !important;}.accordion {--bs-accordion-border-radius: 10px;--bs-accordion-inner-border-radius: calc(10px - 1px);--bs-accordion-color: var(--bs-body-color);--bs-accordion-active-color: var(--bs-body-color);--bs-accordion-active-bg: white;--bs-accordion-btn-active-icon: url("data:image/svg+xml,
");}.accordion-button:focus {outline: 0;box-shadow: none;}.accordion-body {letter-spacing: -0.01em;}.bb-group {border: 0.6rem solid #f6f6f6;background-color: #f6f6f6;border-radius: 0.5rem;position: relative;display: inline-flex;vertical-align: middle;}.bb-group>.btn {--bs-btn-padding-x: 0.5rem;--bs-btn-padding-y: 0.22rem;--bs-btn-border-radius: 0.3rem;--bs-btn-border-width: 0;color: #545454;}.bb-group>.btn-check:checked+.btn, .bb-group .btn.active {color: black;font-weight: bold;background-color: white;}.paging {display: flex;}.paging .bb-group>.btn {min-width: 2rem;margin-left: 0.1rem;margin-right: 0.1rem;}.paging .bb-group>.btn:hover {background-color: white;}.paging a {text-decoration: none;}.btn-paging {--bs-btn-color: #757575;--bs-btn-border-color: #e2e2e2;--bs-btn-hover-color: black;--bs-btn-hover-bg: #f6f6f6;--bs-btn-hover-border-color: #e2e2e2;--bs-btn-focus-shadow-rgb: 108, 117, 125;--bs-btn-active-color: #fff;--bs-btn-active-bg: #e2e2e2;--bs-btn-active-border-color: #e2e2e2;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-gradient: none;--bs-btn-padding-y: 0.75rem;--bs-btn-padding-x: 1.1rem;--bs-btn-border-radius: 0.5rem;--bs-btn-font-weight: bold;background-color: #f6f6f6;}span.btn-paging {cursor: initial;}span.btn-paging:hover {color: #757575;}.btn-paging.active:hover {background-color: white;}.paging-group {border: 1px solid #e2e2e2;border-radius: 0.5rem;}.paging-group>.bb-group {border: 0.53rem solid #f6f6f6;}#wrap {min-height: 100%;height: auto;padding: 112px 0 75px 0;margin: 0 auto -56px;}#footer {background-color: black;color: #757575;height: 56px;overflow: hidden;}.navbar-form {width: 60%;}.navbar-form button {margin-left: -50px;position: relative;}.search-icon {width: 16px;height: 16px;position: absolute;top: 16px;background-size: cover;background-image: url("data:image/svg+xml, %3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7.24976 12.5C10.1493 12.5 12.4998 10.1495 12.4998 7.25C12.4998 4.35051 10.1493 2 7.24976 2C4.35026 2 1.99976 4.35051 1.99976 7.25C1.99976 10.1495 4.35026 12.5 7.24976 12.5Z' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3Cpath d='M10.962 10.9625L13.9996 14.0001' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E");}.navbar-form ::placeholder {color: #e2e2e2;}.ellipsis {overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}.data-table {table-layout: fixed;overflow-wrap: anywhere;margin-left: 8px;margin-top: 2rem;margin-bottom: 2rem;width: calc(100% - 16px);}.data-table thead {padding-bottom: 20px;}.table.data-table> :not(caption)>*>* {padding: 0.8rem 0.8rem;background-color: var(--bs-table-bg);border-bottom-width: 1px;box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg);}.table.data-table>thead>*>* {padding-bottom: 1.5rem;}.table.data-table>*>*:last-child>* {border-bottom: none;}.data-table thead, .data-table thead tr, .data-table thead th {color: #757575;border: none;font-weight: normal;}.data-table tbody th {color: #757575;font-weight: normal;}.data-table tbody {background: white;border-radius: 8px;box-shadow: 0 0 0 8px white;}.data-table h3, .data-table h5, .data-table h6 {margin-bottom: 0;}.data-table h3, .data-table h5 {color: var(--bs-body-color);}.accordion .table.data-table>thead>*>* {padding-bottom: 0;}.info-table tbody {display: inline-table;width: 100%;}.info-table td {font-weight: bold;}.info-table tr>td:first-child {font-weight: normal;color: #757575;}.ns:before {content: " ";}.nc:before {content: ",";}.trezor-logo {width: 128px;height: 32px;position: absolute;top: 16px;background-size: cover;background-image: url("data:image/svg+xml,%3Csvg style='width: 128px%3B' version='1.1' xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 163.7 41.9' space='preserve'%3E%3Cpolygon points='101.1 12.8 118.2 12.8 118.2 17.3 108.9 29.9 118.2 29.9 118.2 35.2 101.1 35.2 101.1 30.7 110.4 18.1 101.1 18.1'%3E%3C/polygon%3E%3Cpath d='M158.8 26.9c2.1-0.8 4.3-2.9 4.3-6.6c0-4.5-3.1-7.4-7.7-7.4h-10.5v22.3h5.8v-7.5h2.2l4.1 7.5h6.7L158.8 26.9z M154.7 22.5h-4V18h4c1.5 0 2.5 0.9 2.5 2.2C157.2 21.6 156.2 22.5 154.7 22.5z'%3E%3C/path%3E%3Cpath d='M130.8 12.5c-6.8 0-11.6 4.9-11.6 11.5s4.9 11.5 11.6 11.5s11.7-4.9 11.7-11.5S137.6 12.5 130.8 12.5z M130.8 30.3c-3.4 0-5.7-2.6-5.7-6.3c0-3.8 2.3-6.3 5.7-6.3c3.4 0 5.8 2.6 5.8 6.3C136.6 27.7 134.2 30.3 130.8 30.3z'%3E%3C/path%3E%3Cpolygon points='82.1 12.8 98.3 12.8 98.3 18 87.9 18 87.9 21.3 98 21.3 98 26.4 87.9 26.4 87.9 30 98.3 30 98.3 35.2 82.1 35.2'%3E%3C/polygon%3E%3Cpath d='M24.6 9.7C24.6 4.4 20 0 14.4 0S4.2 4.4 4.2 9.7v3.1H0v22.3h0l14.4 6.7l14.4-6.7h0V12.9h-4.2V9.7z M9.4 9.7c0-2.5 2.2-4.5 5-4.5s5 2 5 4.5v3.1H9.4V9.7z M23 31.5l-8.6 4l-8.6-4V18.1H23V31.5z'%3E%3C/path%3E%3Cpath d='M79.4 20.3c0-4.5-3.1-7.4-7.7-7.4H61.2v22.3H67v-7.5h2.2l4.1 7.5H80l-4.9-8.3C77.2 26.1 79.4 24 79.4 20.3z M71 22.5h-4V18h4c1.5 0 2.5 0.9 2.5 2.2C73.5 21.6 72.5 22.5 71 22.5z'%3E%3C/path%3E%3Cpolygon points='40.5 12.8 58.6 12.8 58.6 18.1 52.4 18.1 52.4 35.2 46.6 35.2 46.6 18.1 40.5 18.1'%3E%3C/polygon%3E%3C/svg%3E");}.copyable::before, .copied::before {width: 18px;height: 16px;margin: 3px -18px;content: "";position: absolute;background-size: cover;}.copyable::before {display: none;cursor: copy;background-image: url("data:image/svg+xml,%3Csvg width='18' height='16' viewBox='0 0 18 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10.5 10.4996H13.5V2.49963H5.5V5.49963' stroke='%2300854D' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M10.4998 5.49976H2.49976V13.4998H10.4998V5.49976Z' stroke='%2300854D' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");}.copyable:hover::before {display: inline-block;}.copied::before {transition: all 0.4s ease;transform: scale(1.2);background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='16' viewBox='-30 -30 330 330'%3E%3Cpath d='M 30,180 90,240 240,30' style='stroke:%2300854D;stroke-width:32;fill:none'/%3E%3C/svg%3E");}.h-data {letter-spacing: 0.12em;font-weight: normal !important;}.tx-detail {background: #f6f6f6;color: #757575;border-radius: 10px;box-shadow: 0 0 0 10px white;width: calc(100% - 20px);margin-left: 10px;margin-top: 3rem;overflow-wrap: break-word;}.tx-detail:first-child {margin-top: 1rem;}.tx-detail:last-child {margin-bottom: 2rem;}.tx-detail span.ellipsis, .tx-detail a.ellipsis {display: block;float: left;max-width: 100%;}.tx-detail>.head, .tx-detail>.footer {padding: 1.5rem;--bs-gutter-x: 0;}.tx-detail>.head {border-radius: 10px 10px 0 0;}.tx-detail .txid {font-size: 106%;letter-spacing: -0.01em;}.tx-detail>.body {padding: 0 1.5rem;--bs-gutter-x: 0;letter-spacing: -0.01em;}.tx-detail>.subhead {padding: 1.5rem 1.5rem 0.4rem 1.5rem;--bs-gutter-x: 0;letter-spacing: 0.1em;text-transform: uppercase;color: var(--bs-body-color);}.tx-detail>.subhead-2 {padding: 0.3rem 1.5rem 0 1.5rem;--bs-gutter-x: 0;font-size: .875em;color: var(--bs-body-color);}.tx-in .col-12, .tx-out .col-12, .tx-addr .col-12 {background-color: white;padding: 1.2rem 1.3rem;border-bottom: 1px solid #f6f6f6;}.amt-out {padding: 1.2rem 0 1.2rem 1rem;text-align: right;overflow-wrap: break-word;}.tx-in .col-12:last-child, .tx-out .col-12:last-child {border-bottom: none;}.tx-own {background-color: #fff9e3 !important;}.tx-amt {float: right !important;}.spent {color: #dc3545 !important;}.unspent {color: #28a745 !important;}.outpoint {color: #757575 !important;}.spent, .unspent, .outpoint {display: inline-block;text-align: right;min-width: 18px;text-decoration: none !important;}.octicon {height: 24px;width: 24px;margin-left: -12px;margin-top: 19px;position: absolute;background-size: cover;background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9 4.5L16.5 12L9 19.5' stroke='%23AFAFAF' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");}.txvalue {color: var(--bs-body-color);font-weight: bold;}.txerror {color: #c51f13;}.txerror a, .txerror .txvalue {color: #c51f13;}.txerror .copyable::before, .txerror .copied::before {filter: invert(86%) sepia(43%) saturate(732%) hue-rotate(367deg) brightness(84%);}.tx-amt .amt:hover, .tx-amt.amt:hover, .amt-out>.amt:hover {color: var(--bs-body-color);}.prim-amt {display: initial;}.sec-amt {display: none;}.csec-amt {display: none;}.base-amt {display: none;}.cbase-amt {display: none;}.tooltip {--bs-tooltip-opacity: 1;--bs-tooltip-max-width: 380px;--bs-tooltip-bg: #fff;--bs-tooltip-color: var(--bs-body-color);--bs-tooltip-padding-x: 1rem;--bs-tooltip-padding-y: 0.8rem;filter: drop-shadow(0px 24px 64px rgba(22, 27, 45, 0.25));}.l-tooltip {text-align: start;display: inline-block;}.l-tooltip .prim-amt, .l-tooltip .sec-amt, .l-tooltip .csec-amt, .l-tooltip .base-amt, .l-tooltip .cbase-amt {display: initial;float: right;}.l-tooltip .amt-time {padding-right: 3rem;float: left;}.amt-dec {font-size: 95%;}.unconfirmed {color: white;background-color: #c51e13;padding: 0.7rem 1.2rem;border-radius: 1.4rem;}.json {word-wrap: break-word;font-size: smaller;background: #002b31;border-radius: 8px;}#raw {padding: 1.5rem 2rem;color: #ffffff;letter-spacing: 0.02em;}#raw .string {color: #2bca87;}#raw .number, #raw .boolean {color: #efc941;}#raw .null {color: red;}@media (max-width: 768px) {body {font-size: 0.8rem;background: linear-gradient(to bottom, #f6f6f6 500px, #e5e5e5 0), #e5e5e5;}.container {padding-left: 2px;padding-right: 2px;}.accordion-body {padding: var(--bs-accordion-body-padding-y) 0;}.octicon {scale: 60% !important;margin-top: -2px;}.unconfirmed {padding: 0.1rem 0.8rem;}.btn {--bs-btn-font-size: 0.8rem;}}@media (max-width: 991px) {#header .container {min-height: 40px;}#header .dropdown-menu[data-bs-popper] {left: 0;right: initial;}.trezor-logo {top: 10px;}.octicon {scale: 80%;}.table.data-table>:not(caption)>*>* {padding: 0.8rem 0.4rem;}.tx-in .col-12, .tx-out .col-12, .tx-addr .col-12 {padding: 0.7rem 1.1rem;}.amt-out {padding: 0.7rem 0 0.7rem 1rem }}@media (min-width: 769px) {body {font-size: 0.9rem;}.btn {--bs-btn-font-size: 0.9rem;}}@media (min-width: 1200px) {.h1, h1 {font-size: 2.4rem;}body {font-size: 1rem;}.btn {--bs-btn-font-size: 1rem;}}
\ No newline at end of file
+@import "TTHoves/TTHoves.css";* {margin: 0;padding: 0;outline: none;font-family: "TT Hoves", -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif;}html, body {height: 100%;}body {min-height: 100%;margin: 0;background: linear-gradient(to bottom, #f6f6f6 360px, #e5e5e5 0), #e5e5e5;background-repeat: no-repeat;}a {color: #00854d;text-decoration: none;}a:hover {color: #00854d;text-decoration: underline;}select {border-radius: 0.5rem;padding-left: 0.5rem;border: 1px solid #ced4da;color: var(--bs-body-color);min-height: 45px;}#header {position: fixed;top: 0;left: 0;width: 100%;margin: 0;padding-bottom: 0;padding-top: 0;background-color: white;border-bottom: 1px solid #f6f6f6;z-index: 10;}#header a {color: var(--bs-navbar-brand-color);}#header a:hover {color: var(--bs-navbar-brand-hover-color);}#header .navbar {--bs-navbar-padding-y: 0.7rem;}#header .form-control-lg {font-size: 1rem;padding: 0.75rem 1rem;}#header .container {min-height: 50px;}#header .btn.dropdown-toggle {padding-right: 0;}#header .dropdown-menu {--bs-dropdown-min-width: 13rem;}#header .dropdown-menu[data-bs-popper] {left: initial;right: 0;}#header .dropdown-menu.show {display: flex;}.form-control:focus {outline: 0;box-shadow: none;border-color: #00854d;}.base-value {color: #757575 !important;padding-left: 0.5rem;font-weight: normal;}.badge {vertical-align: middle;text-transform: uppercase;letter-spacing: 0.15em;--bs-badge-padding-x: 0.8rem;--bs-badge-font-weight: normal;--bs-badge-border-radius: 0.6rem;}.bg-secondary {background-color: #757575 !important;}.accordion {--bs-accordion-border-radius: 10px;--bs-accordion-inner-border-radius: calc(10px - 1px);--bs-accordion-color: var(--bs-body-color);--bs-accordion-active-color: var(--bs-body-color);--bs-accordion-active-bg: white;--bs-accordion-btn-active-icon: url("data:image/svg+xml,
");}.accordion-button:focus {outline: 0;box-shadow: none;}.accordion-body {letter-spacing: -0.01em;}.bb-group {border: 0.6rem solid #f6f6f6;background-color: #f6f6f6;border-radius: 0.5rem;position: relative;display: inline-flex;vertical-align: middle;}.bb-group>.btn {--bs-btn-padding-x: 0.5rem;--bs-btn-padding-y: 0.22rem;--bs-btn-border-radius: 0.3rem;--bs-btn-border-width: 0;color: #545454;}.bb-group>.btn-check:checked+.btn, .bb-group .btn.active {color: black;font-weight: bold;background-color: white;}.paging {display: flex;}.paging .bb-group>.btn {min-width: 2rem;margin-left: 0.1rem;margin-right: 0.1rem;}.paging .bb-group>.btn:hover {background-color: white;}.paging a {text-decoration: none;}.btn-paging {--bs-btn-color: #757575;--bs-btn-border-color: #e2e2e2;--bs-btn-hover-color: black;--bs-btn-hover-bg: #f6f6f6;--bs-btn-hover-border-color: #e2e2e2;--bs-btn-focus-shadow-rgb: 108, 117, 125;--bs-btn-active-color: #fff;--bs-btn-active-bg: #e2e2e2;--bs-btn-active-border-color: #e2e2e2;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-gradient: none;--bs-btn-padding-y: 0.75rem;--bs-btn-padding-x: 1.1rem;--bs-btn-border-radius: 0.5rem;--bs-btn-font-weight: bold;background-color: #f6f6f6;}span.btn-paging {cursor: initial;}span.btn-paging:hover {color: #757575;}.btn-paging.active:hover {background-color: white;}.paging-group {border: 1px solid #e2e2e2;border-radius: 0.5rem;}.paging-group>.bb-group {border: 0.53rem solid #f6f6f6;}#wrap {min-height: 100%;height: auto;padding: 112px 0 75px 0;margin: 0 auto -56px;}#footer {background-color: black;color: #757575;height: 56px;overflow: hidden;}.navbar-form {width: 60%;}.navbar-form button {margin-left: -50px;position: relative;}.search-icon {width: 16px;height: 16px;position: absolute;top: 16px;background-size: cover;background-image: url("data:image/svg+xml, %3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7.24976 12.5C10.1493 12.5 12.4998 10.1495 12.4998 7.25C12.4998 4.35051 10.1493 2 7.24976 2C4.35026 2 1.99976 4.35051 1.99976 7.25C1.99976 10.1495 4.35026 12.5 7.24976 12.5Z' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3Cpath d='M10.962 10.9625L13.9996 14.0001' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E");}.navbar-form ::placeholder {color: #e2e2e2;}.ellipsis {overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}.data-table {table-layout: fixed;overflow-wrap: anywhere;margin-left: 8px;margin-top: 2rem;margin-bottom: 2rem;width: calc(100% - 16px);}.data-table thead {padding-bottom: 20px;}.table.data-table> :not(caption)>*>* {padding: 0.8rem 0.8rem;background-color: var(--bs-table-bg);border-bottom-width: 1px;box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg);}.table.data-table>thead>*>* {padding-bottom: 1.5rem;}.table.data-table>*>*:last-child>* {border-bottom: none;}.data-table thead, .data-table thead tr, .data-table thead th {color: #757575;border: none;font-weight: normal;}.data-table tbody th {color: #757575;font-weight: normal;}.data-table tbody {background: white;border-radius: 8px;box-shadow: 0 0 0 8px white;}.data-table h3, .data-table h5, .data-table h6 {margin-bottom: 0;}.data-table h3, .data-table h5 {color: var(--bs-body-color);}.accordion .table.data-table>thead>*>* {padding-bottom: 0;}.info-table tbody {display: inline-table;width: 100%;}.info-table td {font-weight: bold;}.info-table tr>td:first-child {font-weight: normal;color: #757575;}.ns:before {content: " ";}.nc:before {content: ",";}.trezor-logo {height: 32px;position: absolute;top: 16px;background-size: contain;background-repeat: no-repeat;}.copyable::before, .copied::before {width: 18px;height: 16px;margin: 3px -18px;content: "";position: absolute;background-size: cover;}.copyable::before {display: none;cursor: copy;background-image: url("data:image/svg+xml,%3Csvg width='18' height='16' viewBox='0 0 18 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10.5 10.4996H13.5V2.49963H5.5V5.49963' stroke='%2300854D' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M10.4998 5.49976H2.49976V13.4998H10.4998V5.49976Z' stroke='%2300854D' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");}.copyable:hover::before {display: inline-block;}.copied::before {transition: all 0.4s ease;transform: scale(1.2);background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='16' viewBox='-30 -30 330 330'%3E%3Cpath d='M 30,180 90,240 240,30' style='stroke:%2300854D;stroke-width:32;fill:none'/%3E%3C/svg%3E");}.h-data {letter-spacing: 0.12em;font-weight: normal !important;}.tx-detail {background: #f6f6f6;color: #757575;border-radius: 10px;box-shadow: 0 0 0 10px white;width: calc(100% - 20px);margin-left: 10px;margin-top: 3rem;overflow-wrap: break-word;}.tx-detail:first-child {margin-top: 1rem;}.tx-detail:last-child {margin-bottom: 2rem;}.tx-detail span.ellipsis, .tx-detail a.ellipsis {display: block;float: left;max-width: 100%;}.tx-detail>.head, .tx-detail>.footer {padding: 1.5rem;--bs-gutter-x: 0;}.tx-detail>.head {border-radius: 10px 10px 0 0;}.tx-detail .txid {font-size: 106%;letter-spacing: -0.01em;}.tx-detail>.body {padding: 0 1.5rem;--bs-gutter-x: 0;letter-spacing: -0.01em;}.tx-detail>.subhead {padding: 1.5rem 1.5rem 0.4rem 1.5rem;--bs-gutter-x: 0;letter-spacing: 0.1em;text-transform: uppercase;color: var(--bs-body-color);}.tx-detail>.subhead-2 {padding: 0.3rem 1.5rem 0 1.5rem;--bs-gutter-x: 0;font-size: .875em;color: var(--bs-body-color);}.tx-in .col-12, .tx-out .col-12, .tx-addr .col-12 {background-color: white;padding: 1.2rem 1.3rem;border-bottom: 1px solid #f6f6f6;}.amt-out {padding: 1.2rem 0 1.2rem 1rem;text-align: right;overflow-wrap: break-word;}.tx-in .col-12:last-child, .tx-out .col-12:last-child {border-bottom: none;}.tx-own {background-color: #fff9e3 !important;}.tx-amt {float: right !important;}.spent {color: #dc3545 !important;}.unspent {color: #28a745 !important;}.outpoint {color: #757575 !important;}.spent, .unspent, .outpoint {display: inline-block;text-align: right;min-width: 18px;text-decoration: none !important;}.octicon {height: 24px;width: 24px;margin-left: -12px;margin-top: 19px;position: absolute;background-size: cover;background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9 4.5L16.5 12L9 19.5' stroke='%23AFAFAF' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");}.txvalue {color: var(--bs-body-color);font-weight: bold;}.txerror {color: #c51f13;}.txerror a, .txerror .txvalue {color: #c51f13;}.txerror .copyable::before, .txerror .copied::before {filter: invert(86%) sepia(43%) saturate(732%) hue-rotate(367deg) brightness(84%);}.tx-amt .amt:hover, .tx-amt.amt:hover, .amt-out>.amt:hover {color: var(--bs-body-color);}.prim-amt {display: initial;}.sec-amt {display: none;}.csec-amt {display: none;}.base-amt {display: none;}.cbase-amt {display: none;}.tooltip {--bs-tooltip-opacity: 1;--bs-tooltip-max-width: 380px;--bs-tooltip-bg: #fff;--bs-tooltip-color: var(--bs-body-color);--bs-tooltip-padding-x: 1rem;--bs-tooltip-padding-y: 0.8rem;filter: drop-shadow(0px 24px 64px rgba(22, 27, 45, 0.25));}.l-tooltip {text-align: start;display: inline-block;}.l-tooltip .prim-amt, .l-tooltip .sec-amt, .l-tooltip .csec-amt, .l-tooltip .base-amt, .l-tooltip .cbase-amt {display: initial;float: right;}.l-tooltip .amt-time {padding-right: 3rem;float: left;}.amt-dec {font-size: 95%;}.unconfirmed {color: white;background-color: #c51e13;padding: 0.7rem 1.2rem;border-radius: 1.4rem;}.json {word-wrap: break-word;font-size: smaller;background: #002b31;border-radius: 8px;}#raw {padding: 1.5rem 2rem;color: #ffffff;letter-spacing: 0.02em;}#raw .string {color: #2bca87;}#raw .number, #raw .boolean {color: #efc941;}#raw .null {color: red;}@media (max-width: 768px) {body {font-size: 0.8rem;background: linear-gradient(to bottom, #f6f6f6 500px, #e5e5e5 0), #e5e5e5;}.container {padding-left: 2px;padding-right: 2px;}.accordion-body {padding: var(--bs-accordion-body-padding-y) 0;}.octicon {scale: 60% !important;margin-top: -2px;}.unconfirmed {padding: 0.1rem 0.8rem;}.btn {--bs-btn-font-size: 0.8rem;}}@media (max-width: 991px) {#header .container {min-height: 40px;}#header .dropdown-menu[data-bs-popper] {left: 0;right: initial;}.trezor-logo {top: 10px;}.octicon {scale: 80%;}.table.data-table>:not(caption)>*>* {padding: 0.8rem 0.4rem;}.tx-in .col-12, .tx-out .col-12, .tx-addr .col-12 {padding: 0.7rem 1.1rem;}.amt-out {padding: 0.7rem 0 0.7rem 1rem }}@media (min-width: 769px) {body {font-size: 0.9rem;}.btn {--bs-btn-font-size: 0.9rem;}}@media (min-width: 1200px) {.h1, h1 {font-size: 2.4rem;}body {font-size: 1rem;}.btn {--bs-btn-font-size: 1rem;}}
\ No newline at end of file
diff --git a/static/img/logo.svg b/static/img/logo.svg
new file mode 100644
index 0000000000..4b1c070fee
--- /dev/null
+++ b/static/img/logo.svg
@@ -0,0 +1,9 @@
+
diff --git a/static/templates/base.html b/static/templates/base.html
index 9156d494e1..a2f43fba16 100644
--- a/static/templates/base.html
+++ b/static/templates/base.html
@@ -5,13 +5,14 @@
+
-
-
Trezor {{.CoinLabel}} Explorer
+
+
{{.Branding.BrandName}} {{.CoinLabel}} Explorer
@@ -19,8 +20,8 @@