From 4e37fe821021fd3361654ce21461741869fb5ff4 Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Wed, 4 Feb 2026 12:45:04 -0800 Subject: [PATCH] feat(explorer): externalize branding to environ config Fork-specific strings in templates and embeds forced ongoing drift from upstream and made rebases noisy. Defaults stay in configs/environ.json so the tree stays Trezor-first, while gitignored overrides and static assets let operators brand without forking markup or rebuilding embeds. --- .gitignore | 5 +- api/embed/about | 1 - api/embed/tos_link | 1 - api/text.go | 33 +--- blockbook.go | 12 +- common/branding.go | 301 +++++++++++++++++++++++++++++ common/branding_test.go | 228 ++++++++++++++++++++++ configs/environ.json | 28 ++- docs/build.md | 10 + docs/config.md | 12 +- server/public.go | 11 +- server/public_ethereumtype_test.go | 8 +- server/public_test.go | 50 +++-- static/css/main.css | 5 +- static/css/main.min.4.css | 2 +- static/img/logo.svg | 9 + static/templates/base.html | 39 ++-- static/templates/index.html | 4 +- 18 files changed, 678 insertions(+), 81 deletions(-) delete mode 100644 api/embed/about delete mode 100644 api/embed/tos_link create mode 100644 common/branding.go create mode 100644 common/branding_test.go create mode 100644 static/img/logo.svg 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 Explorer

Address address7b.eth

0x7B62EB7fe80350DC7EC945C0B73242cb9877FB1b

0.000000000123450123 FAKE
0.00 USD

Confirmed
Balance0.000000000123450123 FAKE0.00 USD
Transactions2
Non-contract Transactions0
Internal Transactions0
Nonce123
ContractQuantityValueTransfers#
Contract 130.000000001000123013 S13-1
Contract 740.001000123074 S74-1
ContractTokensTransfers#
Contract 20511

Transactions

ERC721 Token Transfers
ERC20 Token Transfers
address7b.eth
 
871.180000950184 S74-
 
address7b.eth
7.674999999999991915 S13-
`, + `Trezor Fake Coin Explorer

Address address7b.eth

0x7B62EB7fe80350DC7EC945C0B73242cb9877FB1b

0.000000000123450123 FAKE
0.00 USD

Confirmed
Balance0.000000000123450123 FAKE0.00 USD
Transactions2
Non-contract Transactions0
Internal Transactions0
Nonce123
ContractQuantityValueTransfers#
Contract 130.000000001000123013 S13-1
Contract 740.001000123074 S74-1
ContractTokensTransfers#
Contract 20511

Transactions

ERC721 Token Transfers
ERC20 Token Transfers
address7b.eth
 
871.180000950184 S74-
 
address7b.eth
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 Explorer

Address

0x5Dc6288b35E0807A3d6fEB89b3a2Ff4aB773168e

0.000000000123450093 FAKE
0.00 USD

Confirmed
Balance0.000000000123450093 FAKE0.00 USD
Transactions1
Non-contract Transactions1
Internal Transactions0
Nonce93
ContractTokensTransfers#
Contract 1111 S111 of ID 1776, 10 S111 of ID 18981

Transactions

0x5Dc6288b35E0807A3d6fEB89b3a2Ff4aB773168e
 
0 FAKE0.00 USD0.00 USD
ERC1155 Token Transfers
 
0x5Dc6288b35E0807A3d6fEB89b3a2Ff4aB773168e
1 S111 of ID 1776, 10 S111 of ID 1898
`, + `Trezor Fake Coin Explorer

Address

0x5Dc6288b35E0807A3d6fEB89b3a2Ff4aB773168e

0.000000000123450093 FAKE
0.00 USD

Confirmed
Balance0.000000000123450093 FAKE0.00 USD
Transactions1
Non-contract Transactions1
Internal Transactions0
Nonce93
ContractTokensTransfers#
Contract 1111 S111 of ID 1776, 10 S111 of ID 18981

Transactions

0x5Dc6288b35E0807A3d6fEB89b3a2Ff4aB773168e
 
0 FAKE0.00 USD0.00 USD
ERC1155 Token Transfers
 
0x5Dc6288b35E0807A3d6fEB89b3a2Ff4aB773168e
1 S111 of ID 1776, 10 S111 of ID 1898
`, }, }, { @@ -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 Explorer

Transaction

0xa9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b101
In BlockUnconfirmed
StatusSuccess
Value0 FAKE0.00 USD0.00 USD
Gas Used / Limit52025 / 78037
Gas Price0.00000004 FAKE0.00 USD0.00 USD (40 Gwei)
Max Priority Fee Per Gas0.000000040000000001 FAKE0.00 USD0.00 USD (40.000000001 Gwei)
Max Fee Per Gas0.000000040000000002 FAKE0.00 USD0.00 USD (40.000000002 Gwei)
Base Fee Per Gas0.000000040000000003 FAKE0.00 USD0.00 USD (40.000000003 Gwei)
Fees0.002081 FAKE4.16 USD18.55 USD
RBFON
Nonce208
 
0 FAKE0.00 USD0.00 USD
ERC20 Token Transfers
Input Data

0xa9059cbb000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f00000000000000000000000000000000000000000000021e19e0c9bab2400000
transfer(address, uint256)
#TypeData
0address0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f
1uint25610000000000000000000000
`, + `Trezor Fake Coin Explorer

Transaction

0xa9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b101
In BlockUnconfirmed
StatusSuccess
Value0 FAKE0.00 USD0.00 USD
Gas Used / Limit52025 / 78037
Gas Price0.00000004 FAKE0.00 USD0.00 USD (40 Gwei)
Max Priority Fee Per Gas0.000000040000000001 FAKE0.00 USD0.00 USD (40.000000001 Gwei)
Max Fee Per Gas0.000000040000000002 FAKE0.00 USD0.00 USD (40.000000002 Gwei)
Base Fee Per Gas0.000000040000000003 FAKE0.00 USD0.00 USD (40.000000003 Gwei)
Fees0.002081 FAKE4.16 USD18.55 USD
RBFON
Nonce208
 
0 FAKE0.00 USD0.00 USD
ERC20 Token Transfers
Input Data

0xa9059cbb000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f00000000000000000000000000000000000000000000021e19e0c9bab2400000
transfer(address, uint256)
#TypeData
0address0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f
1uint25610000000000000000000000
`, }, }, { 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

NFT Token Detail

Token ID1
Contract0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9
Contract 205
StandardERC20
`}, + body: []string{`Trezor Fake Coin Explorer

NFT Token Detail

Token ID1
Contract0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9
Contract 205
StandardERC20
`}, }, { 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 Explorer

Transaction

fdd824a780cbb718eeb766eb05d83fdefc793a27082cd5e67f856d69798cf7db
Mined Time1639 days 11 hours ago
In Block00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
In Block Height225494
Total Input0 FAKE
Total Output13.60030331 FAKE
Fees0 FAKE
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
`, + `Trezor Fake Coin Explorer

Transaction

fdd824a780cbb718eeb766eb05d83fdefc793a27082cd5e67f856d69798cf7db
Mined Time1639 days 11 hours ago
In Block00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
In Block Height225494
Total Input0 FAKE
Total Output13.60030331 FAKE
Fees0 FAKE
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
`, }, }, { @@ -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 Explorer

Address

mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz

0.00012345 FAKE

Confirmed
Total Received0.00024690 FAKE
Total Sent0.00012345 FAKE
Final Balance0.00012345 FAKE
No. Transactions2

Transactions

mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE
 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs
 
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE×
`, + `Trezor Fake Coin Explorer

Address

mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz

0.00012345 FAKE

Confirmed
Total Received0.00024690 FAKE
Total Sent0.00012345 FAKE
Final Balance0.00012345 FAKE
No. Transactions2

Transactions

mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE
 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs
 
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 Explorer

Transaction

3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71
Mined Time1639 days 11 hours ago
In Block00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
In Block Height225494
Total Input3172.83951062 FAKE
Total Output3172.83951000 FAKE
Fees0.00000062 FAKE
`, + `Trezor Fake Coin Explorer

Transaction

3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71
Mined Time1639 days 11 hours ago
In Block00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
In Block Height225494
Total Input3172.83951062 FAKE
Total Output3172.83951000 FAKE
Fees0.00000062 FAKE
`, }, }, { @@ -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 Explorer

Error

Transaction not found

`, + `Trezor Fake Coin Explorer

Error

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 Explorer

Blocks

HeightHashTimestampTransactionsSize
22549400000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b61639 days 11 hours ago42345678
2254930000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e29971640 days 9 hours ago21234567
`, + `Trezor Fake Coin Explorer

Blocks

HeightHashTimestampTransactionsSize
22549400000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b61639 days 11 hours ago42345678
2254930000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e29971640 days 9 hours ago21234567
`, }, }, { @@ -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 Explorer

Block

225494
00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
Transactions4
Height225494
Confirmations1
Timestamp1639 days 11 hours ago
Size (bytes)2345678
Version
Merkle Root
Nonce
Bits
Difficulty

Transactions

 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
`, + `Trezor Fake Coin Explorer

Block

225494
00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
Transactions4
Height225494
Confirmations1
Timestamp1639 days 11 hours ago
Size (bytes)2345678
Version
Merkle Root
Nonce
Bits
Difficulty

Transactions

 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
`, }, }, { @@ -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 Explorer

Application status

Synchronization with backend is disabled, the state of index is not up to date.

Blockbook

CoinFakecoin
Host
Version / Commit / Buildunknown / unknown / unknown
Synchronized
true
Last Block225494
Last Block Update`, + `Trezor Fake Coin Explorer

Application status

Synchronization with backend is disabled, the state of index is not up to date.

`, - `

Blockbook

CoinFakecoin
Host
Version / Commit / Buildunknown / unknown / unknown
Synchronized
true
Last Block225494
Last Block Update`, `
Mempool in Sync
false
Last Mempool Update
Transactions in Mempool0
Current Fiat rates

Backend

Chainfakecoin
Version001001
Subversion/Fakecoin:0.0.1/
Last Block2
Difficulty
Blockbook - blockchain indexer for Trezor Suite https://trezor.io/trezor-suite. Do not use for any other purpose.
`, + `

Backend

Chainfakecoin
Version001001
Subversion/Fakecoin:0.0.1/
Last Block2
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 Explorer

Block

225494
00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
Transactions4
Height225494
Confirmations1
Timestamp1639 days 11 hours ago
Size (bytes)2345678
Version
Merkle Root
Nonce
Bits
Difficulty

Transactions

 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
`, + `Trezor Fake Coin Explorer

Block

225494
00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
Transactions4
Height225494
Confirmations1
Timestamp1639 days 11 hours ago
Size (bytes)2345678
Version
Merkle Root
Nonce
Bits
Difficulty

Transactions

 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
`, }, }, { @@ -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 Explorer

Block

225494
00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
Transactions4
Height225494
Confirmations1
Timestamp1639 days 11 hours ago
Size (bytes)2345678
Version
Merkle Root
Nonce
Bits
Difficulty

Transactions

 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
`, + `Trezor Fake Coin Explorer

Block

225494
00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
Transactions4
Height225494
Confirmations1
Timestamp1639 days 11 hours ago
Size (bytes)2345678
Version
Merkle Root
Nonce
Bits
Difficulty

Transactions

 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
`, }, }, { @@ -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 Explorer

Transaction

fdd824a780cbb718eeb766eb05d83fdefc793a27082cd5e67f856d69798cf7db
Mined Time1639 days 11 hours ago
In Block00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
In Block Height225494
Total Input0 FAKE
Total Output13.60030331 FAKE
Fees0 FAKE
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
`, + `Trezor Fake Coin Explorer

Transaction

fdd824a780cbb718eeb766eb05d83fdefc793a27082cd5e67f856d69798cf7db
Mined Time1639 days 11 hours ago
In Block00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
In Block Height225494
Total Input0 FAKE
Total Output13.60030331 FAKE
Fees0 FAKE
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
`, }, }, { @@ -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 Explorer

Address

mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz

0.00012345 FAKE

Confirmed
Total Received0.00024690 FAKE
Total Sent0.00012345 FAKE
Final Balance0.00012345 FAKE
No. Transactions2

Transactions

mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE
 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs
 
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE×
`, + `Trezor Fake Coin Explorer

Address

mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz

0.00012345 FAKE

Confirmed
Total Received0.00024690 FAKE
Total Sent0.00012345 FAKE
Final Balance0.00012345 FAKE
No. Transactions2

Transactions

mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE
 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs
 
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 Explorer

XPUB

upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q

1186.419755 FAKE

Confirmed
Total Received1186.41975501 FAKE
Total Sent0.00000001 FAKE
Final Balance1186.41975500 FAKE
No. Transactions2
Used XPUB Addresses2
XPUB Addresses with Balance
AddressBalanceTxsPath
2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu1186.41975500 FAKE1m/49'/1'/33'/1/3

Transactions

`, + `Trezor Fake Coin Explorer

XPUB

upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q

1186.419755 FAKE

Confirmed
Total Received1186.41975501 FAKE
Total Sent0.00000001 FAKE
Final Balance1186.41975500 FAKE
No. Transactions2
Used XPUB Addresses2
XPUB Addresses with Balance
AddressBalanceTxsPath
2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu1186.41975500 FAKE1m/49'/1'/33'/1/3

Transactions

`, }, }, { @@ -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 Explorer

XPUB

tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{0,1}/*)#4rqwxvej

0 FAKE

Confirmed
Total Received0 FAKE
Total Sent0 FAKE
Final Balance0 FAKE
No. Transactions0
Used XPUB Addresses0
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 Explorer

Error

No matching records found for '1234'

`, + `Trezor Fake Coin Explorer

Error

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

Send Raw Transaction

`, + `Trezor Fake Coin Explorer

Send 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 Explorer

Send Raw Transaction

Invalid data
`, + `Trezor Fake Coin Explorer

Send 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 @@