diff --git a/.github/scripts/make_brand.py b/.github/scripts/make_brand.py new file mode 100644 index 0000000..6fe0e8a --- /dev/null +++ b/.github/scripts/make_brand.py @@ -0,0 +1,272 @@ +"""Regenerate the btc-toolkit brand assets from code. + + assets/icon.svg the Bitcoin logo (public domain), 64 x 64 + assets/logo.svg README header for light backgrounds (also PyPI) + assets/logo-dark.svg README header for GitHub's dark theme + assets/social-preview.png 1280 x 640 banner (GitHub Settings > Social preview) + +The wordmark is JetBrains Mono ExtraBold and the copy IBM Plex Sans, both under +the SIL Open Font License. They are downloaded from their upstream +repositories at pinned commits and checked against a SHA-256 before use; the +SVGs carry the wordmark as outlines, so they look the same without the fonts. +Nothing is drawn by hand: run it again after changing the copy below. + + pip install pillow fonttools + python .github/scripts/make_brand.py +""" + +from __future__ import annotations + +import hashlib +import re +import tempfile +import urllib.request +from pathlib import Path + +from fontTools.pens.svgPathPen import SVGPathPen +from fontTools.pens.transformPen import TransformPen +from fontTools.ttLib import TTFont +from PIL import Image, ImageDraw, ImageFont + +ROOT = Path(__file__).resolve().parents[2] +ASSETS = ROOT / "assets" + +ORANGE = "#F7931A" +INK = "#0D1117" +PANEL = "#161B22" +LINE = "#30363D" +SLATE = "#8B949E" +WHITE = "#FFFFFF" +SNOW = "#E6EDF3" + +_JB = "https://raw.githubusercontent.com/JetBrains/JetBrainsMono/19371302b95d218af43299bce79ddbddd0bc364d/fonts/ttf/" +_PLEX = ("https://raw.githubusercontent.com/IBM/plex/39276912b05f700ee1dd491950c35d8697214208/" + "packages/plex-sans/fonts/complete/ttf/") +FONTS = { + "mono-extrabold": (_JB + "JetBrainsMono-ExtraBold.ttf", + "ee43f7507a07ed15c4bf6211d5cc78fa0bacca58e7afbc170f4b1e38a5d0cde4"), + "mono": (_JB + "JetBrainsMono-Regular.ttf", + "e6fd0d7e91550b3ed2b735d4312474362c4716edc4fc0577a0f61ed782d5aed1"), + "sans-medium": (_PLEX + "IBMPlexSans-Medium.ttf", + "331c8639d7598b2cde62a911a71db195e30cb655cd6bdf2e324a7e984955f907"), + "sans": (_PLEX + "IBMPlexSans-Regular.ttf", + "975dcda37d80f038dcd143c22e33ca2d97a0cc5a929aace1c749153b0fe1afa5"), +} + +# The Bitcoin logo's "B", in a 64 x 64 box (public domain artwork). +BITCOIN_B = ( + "M46.1 27.4c.6-4.2-2.6-6.5-7-8l1.4-5.7-3.5-.9-1.4 5.5c-.9-.2-1.9-.4-2.8-.7l1.4-5.5-3.5-.9-1.4 5.7" + "c-.8-.2-1.5-.3-2.2-.5l0 0-4.8-1.2-.9 3.7s2.6.6 2.5.6c1.4.4 1.7 1.3 1.6 2l-1.6 6.5c.1 0 .2.1.3.1" + "l-.3-.1-2.3 9.1c-.2.4-.6 1.1-1.6.8 0 0-2.5-.6-2.5-.6l-1.7 4 4.5 1.1c.8.2 1.7.4 2.5.7l-1.4 5.8 3.5.9" + " 1.4-5.7c1 .3 1.9.5 2.8.7l-1.4 5.7 3.5.9 1.4-5.8c5.9 1.1 10.4.7 12.3-4.7 1.5-4.3-.1-6.8-3.2-8.4" + " 2.3-.5 4-2.1 4.4-5.2zm-7.9 11.1c-1.1 4.3-8.3 2-10.7 1.4l1.9-7.6c2.3.6 9.9 1.7 8.8 6.2zm1.1-11.2" + "c-1 3.9-7 1.9-9 1.4l1.7-6.9c2 .5 8.3 1.4 7.3 5.5z" +) + +WORDMARK = "btc-toolkit" +HEADLINE = [[("Read and verify Bitcoin", WHITE)], [("from your ", WHITE), ("terminal.", ORANGE)]] +FEATURES = ["OP_RETURN & inscriptions · raw-tx decoding", "offline address checks · Merkle-proof verify"] +INSTALL = "pipx install btc-toolkit" +ALSO = "also uv · brew · pip" + + +def font_file(name: str) -> Path: + url, sha = FONTS[name] + path = Path(tempfile.gettempdir()) / "btc-toolkit-brand-fonts" / url.rsplit("/", 1)[1] + if not path.exists() or hashlib.sha256(path.read_bytes()).hexdigest() != sha: + path.parent.mkdir(parents=True, exist_ok=True) + with urllib.request.urlopen(url, timeout=60) as resp: # noqa: S310 (fixed https URL) + data = resp.read() + if hashlib.sha256(data).hexdigest() != sha: + raise SystemExit(f"{url}: SHA-256 mismatch, refusing to use it") + path.write_bytes(data) + return path + + +# --- the Bitcoin logo as polygons (for the PNG) --------------------------------- + +_ARGS = {"m": 2, "l": 2, "c": 6, "s": 4, "z": 0} + + +def logo_polygons(d: str = BITCOIN_B, steps: int = 24) -> list[list[tuple[float, float]]]: + """Flatten the logo's path (M m l c s z only) into one polygon per subpath.""" + tokens = re.findall(r"[MmLlCcSsZz]|-?(?:\d+\.\d*|\.\d+|\d+)", d) + polys: list[list[tuple[float, float]]] = [] + x = y = sx = sy = 0.0 + last_ctrl: tuple[float, float] | None = None + i, cmd = 0, "" + while i < len(tokens): + if tokens[i].isalpha(): + cmd = tokens[i] + i += 1 + if cmd in "Zz": + x, y = sx, sy + last_ctrl = None + continue + n = _ARGS[cmd.lower()] + a = [float(t) for t in tokens[i:i + n]] + i += n + rel = cmd.islower() + ox, oy = (x, y) if rel else (0.0, 0.0) + if cmd in "Mm": + x, y = ox + a[0], oy + a[1] + sx, sy = x, y + polys.append([(x, y)]) + cmd = "l" if rel else "L" # extra pairs after a moveto are linetos + last_ctrl = None + elif cmd in "Ll": + x, y = ox + a[0], oy + a[1] + polys[-1].append((x, y)) + last_ctrl = None + else: + if cmd in "Cc": + c1 = (ox + a[0], oy + a[1]) + c2, end = (ox + a[2], oy + a[3]), (ox + a[4], oy + a[5]) + else: # Ss: the first control point mirrors the previous curve's second + c1 = (2 * x - last_ctrl[0], 2 * y - last_ctrl[1]) if last_ctrl else (x, y) + c2, end = (ox + a[0], oy + a[1]), (ox + a[2], oy + a[3]) + p0 = (x, y) + for k in range(1, steps + 1): + t = k / steps + u = 1 - t + polys[-1].append(( + u**3 * p0[0] + 3 * u * u * t * c1[0] + 3 * u * t * t * c2[0] + t**3 * end[0], + u**3 * p0[1] + 3 * u * u * t * c1[1] + 3 * u * t * t * c2[1] + t**3 * end[1], + )) + last_ctrl = c2 + x, y = end + return polys + + +def draw_logo(draw: ImageDraw.ImageDraw, left: float, top: float, size: float) -> None: + k = size / 64 + draw.ellipse((left, top, left + size, top + size), fill=ORANGE) + outer, *holes = logo_polygons() + draw.polygon([(left + px * k, top + py * k) for px, py in outer], fill=WHITE) + for hole in holes: + draw.polygon([(left + px * k, top + py * k) for px, py in hole], fill=ORANGE) + + +# --- the social preview ------------------------------------------------------------- + +def social_preview(scale: int = 2) -> Image.Image: + """The 1280 x 640 banner, drawn at `scale`x and downsampled for smooth edges.""" + s = scale + W, H = 1280, 640 + img = Image.new("RGB", (W * s, H * s), INK) + d = ImageDraw.Draw(img) + + def font(name: str, px: float) -> ImageFont.FreeTypeFont: + return ImageFont.truetype(str(font_file(name)), round(px * s)) + + def baseline(top: float, line_h: float, f: ImageFont.FreeTypeFont) -> float: + """Where CSS puts the baseline of a line box `line_h` tall starting at `top`.""" + asc, desc = f.getmetrics() + return top * s + (line_h * s - (asc + desc)) / 2 + asc + + def text(x: float, y_base: float, runs: list[tuple[str, str]], f: ImageFont.FreeTypeFont, + tracking: float = 0.0) -> float: + cx = x * s + for chunk, color in runs: + if tracking: + for ch in chunk: + d.text((cx, y_base), ch, font=f, fill=color, anchor="ls") + cx += f.getlength(ch) + tracking * s + else: + d.text((cx, y_base), chunk, font=f, fill=color, anchor="ls") + cx += f.getlength(chunk) + return cx / s - x + + wordmark, headline, body = font("mono-extrabold", 76), font("sans-medium", 40), font("sans", 21) + mono, small = font("mono", 22), font("mono", 17) + pill_h = (sum(mono.getmetrics()) / s) + 20 + blocks = 76 + 22 + 2 * 48 + 22 + 2 * 31.5 + 30 + pill_h + top = (H - blocks) / 2 + + coin = 248 + draw_logo(d, 88 * s, (H - coin) / 2 * s, coin * s) + + x = 88 + coin + 72 + y = top + text(x, baseline(y, 76, wordmark), [(WORDMARK, WHITE)], wordmark, tracking=-0.03 * 76) + y += 76 + 22 + for line in HEADLINE: + text(x, baseline(y, 48, headline), line, headline) + y += 48 + y += 22 + for feature in FEATURES: + text(x, baseline(y, 31.5, body), [(feature, SLATE)], body) + y += 31.5 + y += 30 + cmd_w = mono.getlength("$ " + INSTALL) / s + d.rounded_rectangle((x * s, y * s, (x + cmd_w + 32) * s, (y + pill_h) * s), radius=10 * s, + fill=PANEL, outline=LINE, width=s) + text(x + 16, baseline(y + 10, pill_h - 20, mono), [("$ ", ORANGE), (INSTALL, WHITE)], mono) + also_x = x + cmd_w + 32 + 18 + also_h = sum(small.getmetrics()) / s + text(also_x, baseline(y + (pill_h - also_h) / 2, also_h, small), [(ALSO, SLATE)], small) + right = also_x + small.getlength(ALSO) / s + if right > W - 56: + raise SystemExit(f"social preview: text ends at x={right:.0f}, past the 56 px margin") + return img.resize((W, H), Image.Resampling.LANCZOS) + + +# --- SVGs --------------------------------------------------------------------------- + +def outline(text: str, font: TTFont, px: float, x: float, base: float, tracking: float) -> tuple[str, float]: + """`text` as one SVG path in `font` at `px`, left edge `x`, baseline `base`; returns (d, width).""" + k = px / font["head"].unitsPerEm + cmap, glyphs, hmtx = font.getBestCmap(), font.getGlyphSet(), font["hmtx"] + parts, cx = [], x + for i, ch in enumerate(text): + name = cmap[ord(ch)] + pen = SVGPathPen(glyphs) + glyphs[name].draw(TransformPen(pen, (k, 0, 0, -k, cx, base))) + parts.append(pen.getCommands()) + cx += hmtx[name][0] * k + (tracking if i < len(text) - 1 else 0) + return " ".join(p for p in parts if p), cx - x + + +def _num(v: float) -> str: + return f"{v:.2f}".rstrip("0").rstrip(".") + + +def logo_svg(wordmark_fill: str) -> str: + font = TTFont(str(font_file("mono-extrabold"))) + pad, coin, gap, px = 2, 88, 22, 58 + hhea = font["hhea"] + em = font["head"].unitsPerEm + content = (hhea.ascent - hhea.descent) / em * px + base = pad + coin / 2 - content / 2 + hhea.ascent / em * px + path, width = outline(WORDMARK, font, px, pad + coin + gap, base, -0.03 * px) + path = re.sub(r"-?\d+\.\d+", lambda m: _num(float(m.group())), path) + w, h = pad + coin + gap + width + pad, coin + 2 * pad + k = coin / 64 + return ( + f'' + f'' + f'' + f'\n' + ) + + +ICON_SVG = ( + '' + f'\n' +) + + +def main() -> None: + ASSETS.mkdir(exist_ok=True) + (ASSETS / "icon.svg").write_text(ICON_SVG, encoding="utf-8") + (ASSETS / "logo.svg").write_text(logo_svg(INK), encoding="utf-8") + (ASSETS / "logo-dark.svg").write_text(logo_svg(SNOW), encoding="utf-8") + social_preview().save(ASSETS / "social-preview.png", optimize=True) + for name in ("icon.svg", "logo.svg", "logo-dark.svg", "social-preview.png"): + print(f"wrote assets/{name}") + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/make_demo.py b/.github/scripts/make_demo.py index d6fe073..ccd2b7d 100644 --- a/.github/scripts/make_demo.py +++ b/.github/scripts/make_demo.py @@ -48,6 +48,8 @@ "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf", 0), ] +ANSI_256 = {208: (255, 135, 0)} # the banner's Bitcoin orange + SGR = re.compile(r"\x1b\[([0-9;]*)m") OTHER_ESCAPES = re.compile(r"\x1b\[[0-9;?]*[A-Za-ln-z]") @@ -95,8 +97,13 @@ def parse(text: str) -> list[Line]: if m is None: break pos = m.end() - for code in [int(c) if c else 0 for c in m.group(1).split(";")]: - if code == 0: + codes = [int(c) if c else 0 for c in m.group(1).split(";")] + while codes: + code = codes.pop(0) + if code == 38 and codes[:1] == [5] and len(codes) > 1: # 256-colour foreground + fg = ANSI_256.get(codes[1], FG) + del codes[:2] + elif code == 0: fg, bold, dim = None, False, False elif code == 1: bold = True diff --git a/CHANGELOG.md b/CHANGELOG.md index a23775b..c5fa5d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ versioning follows [SemVer](https://semver.org/). The `--json` output follows th ## [Unreleased] +## [1.8.1] — 2026-10-15 + +A new look, and a PyPI page that describes 1.8.0. The commands and the `--json` output are the same as in 1.8.0. + +### Changed +- New visual identity, built on the Bitcoin logo and its orange (`#F7931A`): `assets/icon.svg`, a README header + for GitHub's light and dark themes (`assets/logo.svg`, `assets/logo-dark.svg`) and the social preview. They are + generated by `.github/scripts/make_brand.py` from pinned, hash-checked fonts (JetBrains Mono, IBM Plex Sans; + SIL Open Font License), so the images can be rebuilt and reviewed from code. +- The banner at the top of the text output is Bitcoin orange (256-colour palette) instead of cyan. `NO_COLOR` and + piped output still print it without colour. +- The PyPI summary and the README's opening lines describe what the tool does since 1.8.0: read and verify Bitcoin + from the terminal (OP_RETURN, inscriptions, raw transactions, offline address checks, Merkle-proof verify). +- README: btc-toolkit's listing among the developer tools in Jameson Lopp's Bitcoin resources. + ## [1.8.0] — 2026-10-15 Four new commands (`verify`, `decode`, `validate`, `inscription`), `tx --inputs/--outputs`, and fixes from two @@ -273,7 +288,8 @@ First stable release: `opreturn`, `balance`, `fees`, `block`, `utxo` — the ori - 0.4.0 — block explorer · 0.3.0 — fee estimator · 0.2.0 — balance checker and unified CLI. - Phase 1 (untagged) — OP_RETURN reader. -[Unreleased]: https://github.com/devdavidejesus/btc-toolkit/compare/v1.8.0...HEAD +[Unreleased]: https://github.com/devdavidejesus/btc-toolkit/compare/v1.8.1...HEAD +[1.8.1]: https://github.com/devdavidejesus/btc-toolkit/compare/v1.8.0...v1.8.1 [1.8.0]: https://github.com/devdavidejesus/btc-toolkit/compare/v1.7.2...v1.8.0 [1.7.2]: https://github.com/devdavidejesus/btc-toolkit/compare/v1.7.1...v1.7.2 [1.7.1]: https://github.com/devdavidejesus/btc-toolkit/compare/v1.7.0...v1.7.1 diff --git a/README.md b/README.md index a26f280..928e16a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@
-btc-toolkit + + + btc-toolkit + -**Bitcoin CLI toolkit — zero dependencies, no Bitcoin Core required.** +**Read and verify Bitcoin from your terminal.** Zero dependencies, no Bitcoin Core required. -Query the Bitcoin network via the [Mempool.space](https://mempool.space) public API, or your own Mempool instance. +Reads the chain through the [Mempool.space](https://mempool.space) public API, or your own Mempool instance, and checks what it can by computation: address checksums offline, and a transaction against its block's Merkle proof and proof of work. [![Tests](https://github.com/devdavidejesus/btc-toolkit/actions/workflows/tests.yml/badge.svg)](https://github.com/devdavidejesus/btc-toolkit/actions/workflows/tests.yml) [![PyPI](https://img.shields.io/pypi/v/btc-toolkit?label=PyPI&color=F7931A&logo=pypi&logoColor=white)](https://pypi.org/project/btc-toolkit/) @@ -221,7 +224,7 @@ btc-toolkit opreturn --raw Known protocol markers are named by their prefix: a Runes Runestone (`OP_RETURN OP_13`), Omni Layer (a payload starting with `omni` — a plain text message can start that way too), and the BIP 141 witness commitment in a coinbase. They are tagged, not decoded. -**Seen in the wild:** the $320M Liquid Network drain (Sept 2026) was negotiated on-chain via OP_RETURN — every message of it decoded with this command, transaction IDs included: [*I Read a $320M Ransom Negotiation From My Terminal*](https://dev.to/devdavidejesus/i-read-a-320m-ransom-negotiation-from-my-terminal-1159). And the wallet holding 562 BTC from the Coldcard hack (July 2026) became a message board: 49 OP_RETURN messages, read with `opreturn --file`: [*49 Messages to a Thief*](https://dev.to/devdavidejesus/49-messages-to-a-thief-what-people-wrote-to-the-coldcard-hackers-bitcoin-wallet-l1n). +**Seen in the wild:** the $320M Liquid Network drain (Sept 2026) was negotiated on-chain via OP_RETURN — every message of it decoded with this command, transaction IDs included: [*I Read a $320M Ransom Negotiation From My Terminal*](https://dev.to/devdavidejesus/i-read-a-320m-ransom-negotiation-from-my-terminal-1159). And the wallet holding 562 BTC from the Coldcard hack (July 2026) became a message board: 49 OP_RETURN messages, read with `opreturn --file`: [*49 Messages to a Thief*](https://dev.to/devdavidejesus/49-messages-to-a-thief-what-people-wrote-to-the-coldcard-hackers-bitcoin-wallet-l1n). btc-toolkit is also listed among the [developer tools](https://www.lopp.net/bitcoin-information/developer-tools.html) in Jameson Lopp's Bitcoin resources. ### Transactions to Try @@ -309,10 +312,10 @@ btc-toolkit/ ├── tests/ # One file per module, plus CLI, completion and robustness tests — all API calls mocked ├── fuzz/ # Atheris fuzzer for the parsers of untrusted data ├── completions/ # Static bash + zsh completions -├── assets/ # Logo, social preview and the README demos +├── assets/ # Logo, icon, social preview and the README demos ├── .github/ │ ├── workflows/ # CI, release, CodeQL, fuzz, Scorecard, weekly live smoke test -│ ├── scripts/ # Release notes, live smoke test, demo recorder +│ ├── scripts/ # Release notes, live smoke test, demo recorder, brand assets │ └── requirements/ # Hash-pinned CI tools ├── docs/ │ ├── json-schema.md # --json output per command + stability policy diff --git a/assets/icon.svg b/assets/icon.svg index 2d9170e..de07ea5 100644 --- a/assets/icon.svg +++ b/assets/icon.svg @@ -1,16 +1 @@ -AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTpmOWU4NGE3Zi0wMTczLTRjM2UtYWRjYy1iODBlNzE1ZDEyYmMAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaApIiAvSd95mhLR2E3Fy/x4AAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDpiMTIwYTcxNC00NzhjLTQ2NGMtODNmMS1iNzdmYTlhYzRkNWZscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNo7RECyb/D50QrjqQWAYyLUQAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggHQR05lTMz30NYvFUM/k8HZrijRKgboZTmOLWGI7r1umkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaEJXY/baDpleJJJm2w/nYGsAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCD7lliw67QSH85MF3mZVxUThKN4ELW9tBHWMf8giWpi92RuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBiWZmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOmY5ZTg0YTdmLTAxNzMtNGMzZS1hZGNjLWI4MGU3MTVkMTJiYy9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOmE4MjI3MzI0LTI5ODctNDljNC1hOWM3LWEwNjA1MmM1YmEzZXJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCAdBHTmVMzPfQ1i8VQz+TwdmuKNEqBuhlOY4tYYjuvW6aJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFgg1Fj9yj9aMxG+XnaYiV8/jxMUDNz7a0phbETLbhTYf06iY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggGLcyUkqpYXOaqr9IWC0NP6nXaW8qobHOsM5nklxvn7B0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQCOb50AmH86SKGbyln4AdNwkENRxAsnsNoXqriBvZ1a2yJTtgonzv/59Cx3gh1kOdZm7dBt2flX8/+42AYg8Zck= - - - - - - - - - - - - - - - + diff --git a/assets/logo-dark.svg b/assets/logo-dark.svg new file mode 100644 index 0000000..f76a03c --- /dev/null +++ b/assets/logo-dark.svg @@ -0,0 +1 @@ + diff --git a/assets/logo.svg b/assets/logo.svg index 77c12b9..c07c7b8 100644 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -1,22 +1 @@ -AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTplYmFlZGRlYy0xZTU0LTRmZjYtYTEyZS04ZWNlODYyNGQ5YzUAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaOkEoITKe3+AyO6nzidZozYAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDpjMzQ3YzNlMS0yMGQzLTQ3NzgtOTA2OC05ZTBkNTg1ODU2YjlscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNo3Vq/mwqNPGUcurSfjWaBNAAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggXd/lmLIbzQfwR4hAWnlhE3bWlqnHLYiUJSR7eJ/eM6mkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaHm0NvJnkLeqC4CU9urH2gkAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCC66GlMjSs3qZt1CwQV0nF4XYE3P7WTdTDuOJUZvuR6qmRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBiWZmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOmViYWVkZGVjLTFlNTQtNGZmNi1hMTJlLThlY2U4NjI0ZDljNS9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOjhkZTc3NTFiLTRlYWMtNGE2Mi1iZjA0LTkyNDFmZDI3MjUwNXJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCBd3+WYshvNB/BHiEBaeWETdtaWqcctiJQlJHt4n94zqaJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggdXztPwxCmTQUoGIJzaAldl41Fl3Jb3gttHEp+yiy5BuiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggg00mO66xvmYZv6iUQrbeyJOKufCSFUCWWLg80vnSnDZ0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQK2Yd78pcTr4/wGRFXUoqmm+1IH5gjYfdR62KMgt/MTL7ExGiXttflsbUCTO7oVtMqbgWZ5k55+2MR5MG+p8n+c= - - - - - - - - - - - - - - - - - - ❯ - btc-toolkit - zero deps · no Core · verify on-chain - + diff --git a/assets/social-preview.png b/assets/social-preview.png index fe8bed4..20ce80a 100644 Binary files a/assets/social-preview.png and b/assets/social-preview.png differ diff --git a/btc_toolkit/__init__.py b/btc_toolkit/__init__.py index efe4520..a16f691 100644 --- a/btc_toolkit/__init__.py +++ b/btc_toolkit/__init__.py @@ -1,3 +1,3 @@ """btc-toolkit — Bitcoin CLI tools. Zero dependencies, no Bitcoin Core required.""" -__version__ = "1.8.0" +__version__ = "1.8.1" diff --git a/btc_toolkit/cli.py b/btc_toolkit/cli.py index 9dfaff0..af051e0 100644 --- a/btc_toolkit/cli.py +++ b/btc_toolkit/cli.py @@ -105,7 +105,7 @@ def _cmd_opreturn(args: argparse.Namespace) -> int: if args.raw: return _opreturn_raw(args) - print(c.cyan(BANNER)) + print(c.orange(BANNER)) print(c.dim(f" btc-toolkit v{__version__} · opreturn · Mempool.space API\n")) txid_short = _terminal_safe(f"{args.txid[:8]}...{args.txid[-8:]}") @@ -201,7 +201,7 @@ def _cmd_balance(args: argparse.Namespace) -> int: if args.json_output: return _balance_json(args) - print(c.cyan(BANNER)) + print(c.orange(BANNER)) print(c.dim(f" btc-toolkit v{__version__} · balance · Mempool.space API\n")) addr_short = args.address if len(args.address) <= 24 else ( @@ -268,7 +268,7 @@ def _cmd_fees(args: argparse.Namespace) -> int: if args.json_output: return _fees_json(args) - print(c.cyan(BANNER)) + print(c.orange(BANNER)) print(c.dim(f" btc-toolkit v{__version__} · fees · Mempool.space API\n")) print(f" {c.bold('Network:')} {args.network}") @@ -317,7 +317,7 @@ def _cmd_block(args: argparse.Namespace) -> int: if args.json_output: return _block_json(args) - print(c.cyan(BANNER)) + print(c.orange(BANNER)) print(c.dim(f" btc-toolkit v{__version__} · block · Mempool.space API\n")) print(f" {c.bold('Block:')} {_terminal_safe(args.ref)}") @@ -379,7 +379,7 @@ def _cmd_utxo(args: argparse.Namespace) -> int: if args.json_output: return _utxo_json(args) - print(c.cyan(BANNER)) + print(c.orange(BANNER)) print(c.dim(f" btc-toolkit v{__version__} · utxo · Mempool.space API\n")) addr_short = args.address if len(args.address) <= 24 else ( @@ -456,7 +456,7 @@ def _cmd_tx(args: argparse.Namespace) -> int: if args.json_output: return _tx_json(args) - print(c.cyan(BANNER)) + print(c.orange(BANNER)) print(c.dim(f" btc-toolkit v{__version__} · tx · Mempool.space API\n")) txid_short = _terminal_safe(f"{args.txid[:8]}...{args.txid[-8:]}") @@ -580,7 +580,7 @@ def _cmd_address(args: argparse.Namespace) -> int: if args.json_output: return _address_json(args) - print(c.cyan(BANNER)) + print(c.orange(BANNER)) print(c.dim(f" btc-toolkit v{__version__} · address · Mempool.space API\n")) addr_short = args.address if len(args.address) <= 24 else ( @@ -640,7 +640,7 @@ def _address_json(args: argparse.Namespace) -> int: # ────────────────────────────────────────────────────────────────────── def _header(command: str, label: str, value: str, network: str, source: str = "Mempool.space API") -> None: - print(c.cyan(BANNER)) + print(c.orange(BANNER)) print(c.dim(f" btc-toolkit v{__version__} · {command} · {source}\n")) print(f" {c.bold(label)} {_terminal_safe(value)}") print(f" {c.bold('Network:')} {network}") diff --git a/btc_toolkit/colors.py b/btc_toolkit/colors.py index ae07015..6b243b3 100644 --- a/btc_toolkit/colors.py +++ b/btc_toolkit/colors.py @@ -34,6 +34,11 @@ def cyan(t: str) -> str: return _c("36", t) +def orange(t: str) -> str: + """Bitcoin orange, from the 256-colour palette (#ff8700): the banner.""" + return _c("38;5;208", t) + + def dim(t: str) -> str: return _c("2", t) diff --git a/pyproject.toml b/pyproject.toml index e65b3f2..cef7442 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta" [project] name = "btc-toolkit" -version = "1.8.0" -description = "Bitcoin CLI toolkit — OP_RETURN decoder, balance checker, fee estimator & more. Zero deps, no Bitcoin Core required." +version = "1.8.1" +description = "Read and verify Bitcoin from your terminal: OP_RETURN, inscriptions, raw transactions, offline address checks, Merkle-proof verify. Zero deps, no Bitcoin Core." readme = "README.md" license = "MIT" license-files = ["LICENSE"] @@ -13,7 +13,7 @@ requires-python = ">=3.11" authors = [ {name = "Davi de Jesus", email = "devdavidejesus@users.noreply.github.com"}, ] -keywords = ["bitcoin", "op_return", "balance", "mempool", "cli", "blockchain"] +keywords = ["bitcoin", "cli", "op_return", "ordinals", "inscriptions", "raw-transaction", "merkle-proof", "bech32", "mempool", "blockchain"] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", diff --git a/tests/test_colors.py b/tests/test_colors.py index 0e670a0..a5c637a 100644 --- a/tests/test_colors.py +++ b/tests/test_colors.py @@ -31,6 +31,11 @@ def test_empty_no_color_is_ignored(self): def test_pipe_has_no_color(self): self._reload(False, {}) self.assertEqual(colors.green("x"), "x") + self.assertEqual(colors.orange("x"), "x") + + def test_orange_is_the_256_colour_bitcoin_orange(self): + self._reload(True, {}) + self.assertEqual(colors.orange("x"), "\033[38;5;208mx\033[0m") if __name__ == "__main__":