35 Commits

Author SHA1 Message Date
867df2f8c9 feat: Add cost to name
All checks were successful
Build Docker / BuildImage (push) Successful in 42s
Check Code Quality / RuffCheck (push) Successful in 57s
2026-02-25 23:16:44 +11:00
9beb3f2918 feat: Add info for address
All checks were successful
Build Docker / BuildImage (push) Successful in 49s
Check Code Quality / RuffCheck (push) Successful in 54s
2026-02-25 23:12:48 +11:00
47e8c24219 feat: Cleanup some text
All checks were successful
Build Docker / BuildImage (push) Successful in 48s
Check Code Quality / RuffCheck (push) Successful in 51s
2026-02-25 23:07:53 +11:00
54ce15baa8 feat: Add custom fonts
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 1m0s
Build Docker / BuildImage (push) Successful in 1m14s
2026-02-25 23:02:49 +11:00
612ead6e63 feat: Move from svg to png
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 45s
Build Docker / BuildImage (push) Successful in 49s
2026-02-25 22:55:41 +11:00
25506da02c feat: Cleanup transaction image
All checks were successful
Build Docker / BuildImage (push) Successful in 38s
Check Code Quality / RuffCheck (push) Successful in 45s
2026-02-25 22:41:21 +11:00
2df0001352 feat: Add commas to block number OG image 2026-02-25 22:31:33 +11:00
f0d233b2f6 feat: Add new OG images
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 2m17s
Build Docker / BuildImage (push) Successful in 2m39s
2026-02-25 22:25:38 +11:00
9a6748b156 feat: Speed up covanent using bulk
Some checks failed
Build Docker / BuildImage (push) Has been cancelled
Check Code Quality / RuffCheck (push) Has been cancelled
2025-11-21 13:34:19 +11:00
90de6042b1 fix: Cleanup TXT record display 2025-11-21 13:28:50 +11:00
eea558361c feat: Add better covenant display
All checks were successful
Build Docker / BuildImage (push) Successful in 38s
Check Code Quality / RuffCheck (push) Successful in 49s
2025-11-21 13:24:21 +11:00
b6662f400a feat: Add support for DATABASE in dockerfile volume
All checks were successful
Build Docker / BuildImage (push) Successful in 39s
Check Code Quality / RuffCheck (push) Successful in 49s
2025-11-21 13:13:35 +11:00
1c51e97354 feat: Add database for namehash caching 2025-11-21 13:11:44 +11:00
206b323be6 feat: Update mobile layout
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 2m0s
Build Docker / BuildImage (push) Successful in 2m13s
2025-11-21 12:45:09 +11:00
400897319f feat: Update OG image
All checks were successful
Build Docker / BuildImage (push) Successful in 42s
Check Code Quality / RuffCheck (push) Successful in 50s
2025-11-21 00:53:07 +11:00
a36e467bd4 Merge pull request 'Redo theming to align with branding' (#1) from feat/gemini_copilot into main
All checks were successful
Build Docker / BuildImage (push) Successful in 39s
Check Code Quality / RuffCheck (push) Successful in 49s
Reviewed-on: #1
2025-11-21 00:39:54 +11:00
f70454c9a4 fix: Add openssl to dockerfile
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 1m5s
Build Docker / BuildImage (push) Successful in 1m10s
2025-11-21 00:29:42 +11:00
3d5d203831 feat: Add hip02 support
All checks were successful
Build Docker / BuildImage (push) Successful in 1m0s
Check Code Quality / RuffCheck (push) Successful in 1m0s
2025-11-21 00:24:51 +11:00
16d7b9f942 feat: Add scroll when viewing a mempool tx 2025-11-20 23:53:43 +11:00
513a3ebd57 feat: Remove coin index and fix mempool lookups 2025-11-20 23:43:53 +11:00
adfa0fd4a0 feat: Add animations 2025-11-20 23:37:48 +11:00
994b5bc5bf feat: Add theming from gemini
All checks were successful
Build Docker / BuildImage (push) Successful in 39s
Check Code Quality / RuffCheck (push) Successful in 49s
2025-11-20 23:31:51 +11:00
99bf4a6a1a fix: Display unowned domains with a message
All checks were successful
Build Docker / BuildImage (push) Successful in 41s
Check Code Quality / RuffCheck (push) Successful in 51s
2025-11-20 18:31:35 +11:00
7c1c6a3a2a feat: Add support for emojis
All checks were successful
Build Docker / BuildImage (push) Successful in 34s
Check Code Quality / RuffCheck (push) Successful in 44s
2025-11-20 18:27:24 +11:00
8f3a455fbb fix: Don't add decimals for stats with block or height
All checks were successful
Build Docker / BuildImage (push) Successful in 38s
Check Code Quality / RuffCheck (push) Successful in 49s
2025-11-20 18:20:17 +11:00
6a8df7d661 fix: Names with different stats
All checks were successful
Build Docker / BuildImage (push) Successful in 37s
Check Code Quality / RuffCheck (push) Successful in 44s
2025-11-20 18:15:13 +11:00
88c3cb9280 fix: Expiry block
All checks were successful
Build Docker / BuildImage (push) Successful in 51s
Check Code Quality / RuffCheck (push) Successful in 55s
2025-11-20 18:09:21 +11:00
edd076e722 feat: Add OpenGraph info
All checks were successful
Build Docker / BuildImage (push) Successful in 33s
Check Code Quality / RuffCheck (push) Successful in 42s
2025-11-20 17:41:41 +11:00
6acdce438b feat: Add og image
All checks were successful
Build Docker / BuildImage (push) Successful in 42s
Check Code Quality / RuffCheck (push) Successful in 53s
2025-11-20 17:39:30 +11:00
7d72c2de0e feat: Add namehash redirect and remove status panel
All checks were successful
Build Docker / BuildImage (push) Successful in 46s
Check Code Quality / RuffCheck (push) Successful in 1m1s
2025-11-20 17:23:05 +11:00
7cb71baf00 feat: Add name info 2025-11-20 17:20:51 +11:00
c04121ed42 feat: Add status api route
All checks were successful
Build Docker / BuildImage (push) Successful in 38s
Check Code Quality / RuffCheck (push) Successful in 46s
2025-11-20 17:18:50 +11:00
2d3ccdaebb fix: Coinbase txs
All checks were successful
Build Docker / BuildImage (push) Successful in 40s
Check Code Quality / RuffCheck (push) Successful in 53s
2025-11-20 17:16:01 +11:00
ba2002574c feat: Add coin rendering
All checks were successful
Build Docker / BuildImage (push) Successful in 34s
Check Code Quality / RuffCheck (push) Successful in 47s
2025-11-20 17:14:08 +11:00
65eea87568 feat: Add rendering for address transactions
All checks were successful
Build Docker / BuildImage (push) Successful in 38s
Check Code Quality / RuffCheck (push) Successful in 47s
2025-11-20 17:07:38 +11:00
15 changed files with 2850 additions and 295 deletions

2
.gitignore vendored
View File

@@ -4,3 +4,5 @@ __pycache__/
.env .env
.vs/ .vs/
.venv/ .venv/
fireexplorer.db
.vscode

View File

@@ -2,7 +2,7 @@ FROM --platform=$BUILDPLATFORM python:3.13-alpine
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
# Install curl for healthcheck # Install curl for healthcheck
RUN apk add --no-cache curl RUN apk add --no-cache curl openssl
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app
@@ -22,6 +22,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
# Add mount point for data volume # Add mount point for data volume
ENV BASE_DIR=/data ENV BASE_DIR=/data
ENV DATABASE_PATH=/data/fireexplorer.db
VOLUME /data VOLUME /data
EXPOSE 5000 EXPOSE 5000

View File

@@ -1,14 +1,16 @@
[project] [project]
name = "python-webserver-template" name = "fireexplorer"
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "A hot new Handshake Blockchain Explorer"
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"cryptography>=46.0.3",
"flask>=3.1.2", "flask>=3.1.2",
"gunicorn>=23.0.0", "gunicorn>=23.0.0",
"pillow>=11.3.0",
"python-dotenv>=1.2.1", "python-dotenv>=1.2.1",
"requests>=2.32.5", "requests-doh>=1.0.0",
] ]
[dependency-groups] [dependency-groups]

View File

@@ -1,5 +1,9 @@
# This file was autogenerated by uv via the following command: # This file was autogenerated by uv via the following command:
# uv export --frozen --output-file=requirements.txt # uv export --frozen --output-file=requirements.txt
anyio==4.11.0 \
--hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \
--hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4
# via httpx
blinker==1.9.0 \ blinker==1.9.0 \
--hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \
--hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc
@@ -7,7 +11,47 @@ blinker==1.9.0 \
certifi==2025.11.12 \ certifi==2025.11.12 \
--hash=sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b \ --hash=sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b \
--hash=sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316 --hash=sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316
# via requests # via
# httpcore
# httpx
# requests
cffi==2.0.0 ; platform_python_implementation != 'PyPy' \
--hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \
--hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \
--hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \
--hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \
--hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \
--hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \
--hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \
--hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \
--hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \
--hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \
--hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \
--hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \
--hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \
--hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \
--hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \
--hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \
--hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \
--hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \
--hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \
--hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \
--hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \
--hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \
--hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \
--hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \
--hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \
--hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \
--hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \
--hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \
--hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \
--hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \
--hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \
--hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \
--hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \
--hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \
--hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5
# via cryptography
cfgv==3.5.0 \ cfgv==3.5.0 \
--hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \ --hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \
--hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132 --hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132
@@ -56,10 +100,62 @@ colorama==0.4.6 ; sys_platform == 'win32' \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
# via click # via click
cryptography==46.0.3 \
--hash=sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217 \
--hash=sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d \
--hash=sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc \
--hash=sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71 \
--hash=sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971 \
--hash=sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a \
--hash=sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926 \
--hash=sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc \
--hash=sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d \
--hash=sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20 \
--hash=sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044 \
--hash=sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3 \
--hash=sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715 \
--hash=sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4 \
--hash=sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506 \
--hash=sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f \
--hash=sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0 \
--hash=sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683 \
--hash=sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3 \
--hash=sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21 \
--hash=sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91 \
--hash=sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c \
--hash=sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8 \
--hash=sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df \
--hash=sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb \
--hash=sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7 \
--hash=sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04 \
--hash=sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db \
--hash=sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459 \
--hash=sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914 \
--hash=sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac \
--hash=sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec \
--hash=sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1 \
--hash=sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb \
--hash=sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac \
--hash=sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665 \
--hash=sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e \
--hash=sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5 \
--hash=sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936 \
--hash=sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de \
--hash=sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372 \
--hash=sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54 \
--hash=sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422 \
--hash=sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849 \
--hash=sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963 \
--hash=sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018
# via fireexplorer
distlib==0.4.0 \ distlib==0.4.0 \
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \ --hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d --hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
# via virtualenv # via virtualenv
dnspython==2.6.1 \
--hash=sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50 \
--hash=sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc
# via requests-doh
filelock==3.20.0 \ filelock==3.20.0 \
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \ --hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4 --hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
@@ -67,11 +163,37 @@ filelock==3.20.0 \
flask==3.1.2 \ flask==3.1.2 \
--hash=sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87 \ --hash=sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87 \
--hash=sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c --hash=sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c
# via python-webserver-template # via fireexplorer
gunicorn==23.0.0 \ gunicorn==23.0.0 \
--hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \ --hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \
--hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec --hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec
# via python-webserver-template # via fireexplorer
h11==0.16.0 \
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
# via httpcore
h2==4.3.0 \
--hash=sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1 \
--hash=sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd
# via dnspython
hpack==4.1.0 \
--hash=sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496 \
--hash=sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca
# via h2
httpcore==1.0.9 \
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
# via
# dnspython
# httpx
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
# via dnspython
hyperframe==6.1.0 \
--hash=sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5 \
--hash=sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08
# via h2
identify==2.6.15 \ identify==2.6.15 \
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \ --hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf --hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
@@ -79,7 +201,10 @@ identify==2.6.15 \
idna==3.11 \ idna==3.11 \
--hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \
--hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902
# via requests # via
# anyio
# httpx
# requests
itsdangerous==2.2.0 \ itsdangerous==2.2.0 \
--hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \ --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \
--hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173 --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173
@@ -146,6 +271,59 @@ packaging==25.0 \
--hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \
--hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f
# via gunicorn # via gunicorn
pillow==12.1.1 \
--hash=sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9 \
--hash=sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da \
--hash=sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f \
--hash=sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642 \
--hash=sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850 \
--hash=sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9 \
--hash=sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8 \
--hash=sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd \
--hash=sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c \
--hash=sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1 \
--hash=sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af \
--hash=sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60 \
--hash=sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986 \
--hash=sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13 \
--hash=sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717 \
--hash=sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b \
--hash=sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15 \
--hash=sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a \
--hash=sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb \
--hash=sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e \
--hash=sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a \
--hash=sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f \
--hash=sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce \
--hash=sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc \
--hash=sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586 \
--hash=sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f \
--hash=sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8 \
--hash=sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60 \
--hash=sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334 \
--hash=sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524 \
--hash=sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf \
--hash=sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2 \
--hash=sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7 \
--hash=sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4 \
--hash=sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b \
--hash=sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c \
--hash=sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e \
--hash=sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029 \
--hash=sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f \
--hash=sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f \
--hash=sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8 \
--hash=sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3 \
--hash=sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e \
--hash=sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36 \
--hash=sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f \
--hash=sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f \
--hash=sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6 \
--hash=sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20 \
--hash=sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202 \
--hash=sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0 \
--hash=sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289
# via fireexplorer
platformdirs==4.5.0 \ platformdirs==4.5.0 \
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \ --hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3 --hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
@@ -153,10 +331,18 @@ platformdirs==4.5.0 \
pre-commit==4.4.0 \ pre-commit==4.4.0 \
--hash=sha256:b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813 \ --hash=sha256:b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813 \
--hash=sha256:f0233ebab440e9f17cabbb558706eb173d19ace965c68cdce2c081042b4fab15 --hash=sha256:f0233ebab440e9f17cabbb558706eb173d19ace965c68cdce2c081042b4fab15
pycparser==2.23 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' \
--hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \
--hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934
# via cffi
pysocks==1.7.1 \
--hash=sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5 \
--hash=sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0
# via requests
python-dotenv==1.2.1 \ python-dotenv==1.2.1 \
--hash=sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6 \ --hash=sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6 \
--hash=sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61 --hash=sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61
# via python-webserver-template # via fireexplorer
pyyaml==6.0.3 \ pyyaml==6.0.3 \
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
@@ -188,10 +374,14 @@ pyyaml==6.0.3 \
--hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6
# via pre-commit # via pre-commit
requests==2.32.5 \ requests==2.32.3 \
--hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
--hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
# via python-webserver-template # via requests-doh
requests-doh==1.0.0 \
--hash=sha256:6ce8bc96245030a198ef20d2100b4dcb3b120a05a58df703f8be121a79f8f2fb \
--hash=sha256:eea6583b792b7d3dfde74fd28eedc2b95d6ea896368119eede31f0d6ff2c838c
# via fireexplorer
ruff==0.14.5 \ ruff==0.14.5 \
--hash=sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68 \ --hash=sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68 \
--hash=sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78 \ --hash=sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78 \
@@ -212,6 +402,10 @@ ruff==0.14.5 \
--hash=sha256:f55382725ad0bdb2e8ee2babcbbfb16f124f5a59496a2f6a46f1d9d99d93e6e2 \ --hash=sha256:f55382725ad0bdb2e8ee2babcbbfb16f124f5a59496a2f6a46f1d9d99d93e6e2 \
--hash=sha256:f66e9bb762e68d66e48550b59c74314168ebb46199886c5c5aa0b0fbcc81b151 \ --hash=sha256:f66e9bb762e68d66e48550b59c74314168ebb46199886c5c5aa0b0fbcc81b151 \
--hash=sha256:f7a75236570318c7a30edd7f5491945f0169de738d945ca8784500b517163a72 --hash=sha256:f7a75236570318c7a30edd7f5491945f0169de738d945ca8784500b517163a72
sniffio==1.3.1 \
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
# via anyio
urllib3==2.5.0 \ urllib3==2.5.0 \
--hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \
--hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc

873
server.py
View File

@@ -4,15 +4,64 @@ from flask import (
render_template, render_template,
send_from_directory, send_from_directory,
send_file, send_file,
jsonify,
g,
request,
) )
import os import os
import requests import requests
import sqlite3
from datetime import datetime from datetime import datetime
from urllib.parse import urlencode
from io import BytesIO
from xml.sax.saxutils import escape
import importlib
import dotenv import dotenv
from tools import hip2, wallet_txt
from werkzeug.middleware.proxy_fix import ProxyFix
dotenv.load_dotenv() dotenv.load_dotenv()
app = Flask(__name__) app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
DATABASE = os.getenv("DATABASE_PATH", "fireexplorer.db")
HSD_API_BASE = os.getenv("HSD_API_BASE", "https://hsd.hns.au/api/v1")
PUBLIC_BASE_URL = os.getenv("PUBLIC_BASE_URL", "").rstrip("/")
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
OG_FONT_DIR = os.path.join(BASE_DIR, "templates", "assets", "fonts")
def get_db():
db = getattr(g, "_database", None)
if db is None:
db = g._database = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, "_database", None)
if db is not None:
db.close()
def init_db():
with app.app_context():
db = get_db()
db.execute(
"""
CREATE TABLE IF NOT EXISTS names (
namehash TEXT PRIMARY KEY,
name TEXT NOT NULL
)
"""
)
db.commit()
init_db()
def find(name, path): def find(name, path):
@@ -21,6 +70,438 @@ def find(name, path):
return os.path.join(root, name) return os.path.join(root, name)
def _truncate(text: str, max_length: int) -> str:
if len(text) <= max_length:
return text
return text[: max_length - 1] + ""
def _safe_hns_value(value) -> str:
try:
return f"{float(value) / 1e6:,.2f} HNS"
except Exception:
return "Unknown"
def _format_int(value, fallback: str = "?") -> str:
try:
return f"{int(value):,}"
except Exception:
return fallback
def _ellipsize_middle(text: str, max_length: int = 48) -> str:
if len(text) <= max_length:
return text
if max_length < 7:
return _truncate(text, max_length)
left = (max_length - 1) // 2
right = max_length - left - 1
return f"{text[:left]}{text[-right:]}"
def _fetch_explorer_json(endpoint: str):
try:
req = requests.get(f"{HSD_API_BASE}/{endpoint}", timeout=4)
if req.status_code == 200:
return req.json(), None
return None, f"HTTP {req.status_code}"
except Exception as e:
return None, str(e)
def _get_public_base_url() -> str:
if PUBLIC_BASE_URL:
return PUBLIC_BASE_URL
forwarded_proto = request.headers.get("X-Forwarded-Proto")
forwarded_host = request.headers.get("X-Forwarded-Host")
if forwarded_proto and forwarded_host:
proto = forwarded_proto.split(",")[0].strip()
host = forwarded_host.split(",")[0].strip()
return f"{proto}://{host}"
return request.url_root.rstrip("/")
def _resolve_namehash(namehash: str) -> str | None:
try:
db = get_db()
cur = db.execute("SELECT name FROM names WHERE namehash = ?", (namehash,))
row = cur.fetchone()
if row and row["name"]:
return row["name"]
except Exception:
pass
try:
req = requests.get(f"{HSD_API_BASE}/namehash/{namehash}", timeout=3)
if req.status_code == 200:
name = req.json().get("result")
if name:
try:
db = get_db()
db.execute(
"INSERT OR REPLACE INTO names (namehash, name) VALUES (?, ?)",
(namehash, name),
)
db.commit()
except Exception:
pass
return name
except Exception:
pass
return None
def _summarize_transaction(tx: dict) -> str:
outputs = tx.get("outputs", []) if isinstance(tx, dict) else []
inputs = tx.get("inputs", []) if isinstance(tx, dict) else []
if not outputs:
return "Transaction details available"
action_counts: dict[str, int] = {}
for output in outputs:
covenant = output.get("covenant") or {}
action = (covenant.get("action") or "NONE").upper()
action_counts[action] = action_counts.get(action, 0) + 1
finalize_count = action_counts.get("FINALIZE", 0)
if finalize_count > 1:
return f"Finalized {finalize_count:,} domains"
# Covenant-aware summary for domain operations (e.g., BID)
for output in outputs:
covenant = output.get("covenant") or {}
action = (covenant.get("action") or "").upper()
if action and action != "NONE":
value = _safe_hns_value(output.get("value"))
items = covenant.get("items") or []
name = None
if items and isinstance(items[0], str):
name = _resolve_namehash(items[0])
if action == "BID":
if name:
return f"Bid {value} on {name}"
return f"Bid {value} on a domain"
if name:
return f"{action.title()}ed {name} • Value {value}"
return f"{action.title()} covenant • Value {value}"
# Detect coinbase transaction
if inputs:
prevout = inputs[0].get("prevout") or {}
if (
prevout.get("hash")
== "0000000000000000000000000000000000000000000000000000000000000000"
and prevout.get("index") == 4294967295
):
reward = _safe_hns_value(sum((o.get("value") or 0) for o in outputs))
return f"Coinbase reward {reward}"
total_output_value = sum((o.get("value") or 0) for o in outputs)
total_output_hns = _safe_hns_value(total_output_value)
recipient_addresses = {
o.get("address") for o in outputs if o.get("address") and o.get("value", 0) > 0
}
recipient_count = len(recipient_addresses)
if recipient_count <= 1:
return f"Sent {total_output_hns}"
return f"Sent a total of {total_output_hns} to {recipient_count} addresses"
def _build_og_context(
search_type: str | None = None, search_value: str | None = None
) -> dict:
default_title = "Fire Explorer"
default_description = "A hot new Handshake Blockchain Explorer"
if not search_type or not search_value:
return {
"title": default_title,
"description": default_description,
"image_query": {"title": default_title, "subtitle": default_description},
}
type_labels = {
"block": "Block",
"header": "Header",
"tx": "Transaction",
"address": "Address",
"name": "Name",
}
label = type_labels.get(search_type, "Search")
fallback_title = f"{label} {search_value} | Fire Explorer"
fallback_description = f"View Handshake {label.lower()} details for {search_value}"
og = {
"title": _truncate(fallback_title, 100),
"description": _truncate(fallback_description, 200),
"image_query": {
"type": label,
"value": search_value,
"title": fallback_title,
"subtitle": fallback_description,
},
}
if search_type == "block":
block, err = _fetch_explorer_json(f"block/{search_value}")
if not err and block:
tx_count = len(block.get("txs", []))
height = block.get("height", "?")
timestamp = block.get("time")
time_str = (
datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M UTC")
if timestamp
else "Unknown time"
)
height_fmt = _format_int(height)
tx_count_fmt = _format_int(tx_count, "0")
subtitle = f"Height {height_fmt}{tx_count_fmt} txs • {time_str}"
og["title"] = _truncate(f"Block {height_fmt} | Fire Explorer", 100)
og["description"] = _truncate(subtitle, 200)
og["image_query"]["subtitle"] = subtitle
elif search_type == "header":
header, err = _fetch_explorer_json(f"header/{search_value}")
if not err and header:
height = header.get("height", "?")
bits = header.get("bits", "?")
subtitle = f"Height {height} • Bits {bits}"
og["title"] = _truncate(f"Header {height} | Fire Explorer", 100)
og["description"] = _truncate(subtitle, 200)
og["image_query"]["subtitle"] = subtitle
elif search_type == "tx":
tx, err = _fetch_explorer_json(f"tx/{search_value}")
if not err and tx:
tx_summary = _summarize_transaction(tx)
fee = _safe_hns_value(tx.get("fee"))
subtitle = f"{tx_summary} • Fee {fee}"
og["title"] = _truncate("Transaction | Fire Explorer", 100)
og["description"] = _truncate(subtitle, 200)
og["image_query"]["subtitle"] = subtitle
elif search_type == "address":
txs, tx_err = _fetch_explorer_json(f"tx/address/{search_value}")
coins, coin_err = _fetch_explorer_json(f"coin/address/{search_value}")
tx_count = len(txs) if isinstance(txs, list) else None
balance_value = (
sum((coin.get("value") or 0) for coin in coins)
if isinstance(coins, list)
else None
)
if tx_count is not None or balance_value is not None:
parts = []
if balance_value is not None:
parts.append(f"Balance {_safe_hns_value(balance_value)}")
if tx_count is not None:
parts.append(f"{_format_int(tx_count, '0')} related transactions")
subtitle = "".join(parts)
og["title"] = _truncate("Address | Fire Explorer", 100)
og["description"] = _truncate(subtitle, 200)
og["image_query"]["subtitle"] = subtitle
elif search_type == "name":
name, err = _fetch_explorer_json(f"name/{search_value}")
if not err and name:
info = name.get("info", name)
state = info.get("state", "Unknown")
owner = info.get("owner", {}).get("hash", "Unknown")
value = info.get("value")
cost_text = _safe_hns_value(value) if value is not None else "Unknown"
subtitle = (
f"State: {state} • Cost: {cost_text} • Owner: {_truncate(owner, 18)}"
)
og["title"] = _truncate(f"Name {search_value} | Fire Explorer", 100)
og["description"] = _truncate(subtitle, 200)
og["image_query"]["subtitle"] = subtitle
og["image_query"]["title"] = og["title"]
return og
def _render_index(search_type: str | None = None, search_value: str | None = None):
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p")
og_context = _build_og_context(search_type, search_value)
image_query = og_context["image_query"]
public_base_url = _get_public_base_url()
og_image_url = public_base_url + "/og-image.png?" + urlencode(image_query)
og_url = public_base_url + request.full_path
if og_url.endswith("?"):
og_url = og_url[:-1]
twitter_domain = request.host
if PUBLIC_BASE_URL and "//" in PUBLIC_BASE_URL:
twitter_domain = PUBLIC_BASE_URL.split("//", 1)[1]
return render_template(
"index.html",
datetime=current_datetime,
meta_title=og_context["title"],
meta_description=og_context["description"],
og_url=og_url,
og_image=og_image_url,
twitter_domain=twitter_domain,
)
def _get_og_image_context() -> dict:
title = _truncate(request.args.get("title", "Fire Explorer"), 90)
subtitle = _truncate(
request.args.get("subtitle", "A hot new Handshake Blockchain Explorer"),
140,
)
search_type = _truncate(request.args.get("type", "Explorer"), 24)
search_value = request.args.get("value", "")
normalized_type = search_type.strip().lower()
value_max_length = 44
if normalized_type in {"transaction", "tx"}:
value_max_length = 30
display_value = _ellipsize_middle(search_value, value_max_length)
if search_value.isdigit() and int(search_value) > 100:
display_value = f"{int(search_value):,}"
value_font_size = "30"
if len(display_value) > 34:
value_font_size = "26"
is_default_card = not search_value.strip() and normalized_type in {
"explorer",
"search",
"",
}
return {
"title": title,
"subtitle": subtitle,
"search_type": search_type,
"search_value": search_value,
"display_value": display_value,
"value_font_size": value_font_size,
"is_default_card": is_default_card,
}
def _load_og_font(size: int, bold: bool = False, mono: bool = False):
try:
image_font_module = importlib.import_module("PIL.ImageFont")
if mono:
candidates = [
os.path.join(OG_FONT_DIR, "DejaVuSansMono.ttf"),
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
"/usr/share/fonts/TTF/DejaVuSansMono.ttf",
"/usr/share/fonts/dejavu/DejaVuSansMono.ttf",
"/usr/share/fonts/truetype/liberation2/LiberationMono-Regular.ttf",
"/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
"DejaVuSansMono.ttf",
]
elif bold:
candidates = [
os.path.join(OG_FONT_DIR, "DejaVuSans-Bold.ttf"),
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/TTF/DejaVuSans-Bold.ttf",
"/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/liberation2/LiberationSans-Bold.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
"DejaVuSans-Bold.ttf",
]
else:
candidates = [
os.path.join(OG_FONT_DIR, "DejaVuSans.ttf"),
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/TTF/DejaVuSans.ttf",
"/usr/share/fonts/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation2/LiberationSans-Regular.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
"DejaVuSans.ttf",
]
for candidate in candidates:
try:
return image_font_module.truetype(candidate, size=size)
except Exception:
continue
return image_font_module.load_default()
except Exception:
try:
image_font_module = importlib.import_module("PIL.ImageFont")
return image_font_module.load_default()
except Exception:
return None
def _fit_text(draw, text: str, font, max_width: int) -> str:
candidate = text
while candidate:
bbox = draw.textbbox((0, 0), candidate, font=font)
if bbox[2] - bbox[0] <= max_width:
return candidate
candidate = candidate[:-1]
if text:
return ""
return text
def _wrap_text(draw, text: str, font, max_width: int, max_lines: int = 2) -> list[str]:
if not text:
return [""]
words = text.split()
if not words:
return [_fit_text(draw, text, font, max_width)]
lines: list[str] = []
current = words[0]
for word in words[1:]:
candidate = f"{current} {word}"
bbox = draw.textbbox((0, 0), candidate, font=font)
if bbox[2] - bbox[0] <= max_width:
current = candidate
continue
lines.append(_fit_text(draw, current, font, max_width))
current = word
if len(lines) >= max_lines - 1:
break
remaining_words = words[len(" ".join(lines + [current]).split()) :]
tail = current
if remaining_words:
tail = f"{current} {' '.join(remaining_words)}"
if len(lines) < max_lines:
lines.append(_fit_text(draw, tail, font, max_width))
return lines[:max_lines]
def _draw_text_shadow(
draw, position: tuple[int, int], text: str, font, fill, shadow=(0, 0, 0, 140)
):
if not text:
return
x, y = position
draw.text((x + 2, y + 2), text, fill=shadow, font=font)
draw.text((x, y), text, fill=fill, font=font)
# Assets routes # Assets routes
@app.route("/assets/<path:path>") @app.route("/assets/<path:path>")
def send_assets(path): def send_assets(path):
@@ -69,38 +550,228 @@ def wellknown(path):
# region Main routes # region Main routes
@app.route("/") @app.route("/")
def index(): def index():
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p") return _render_index()
return render_template("index.html", datetime=current_datetime)
@app.route("/tx/<path:tx_hash>") @app.route("/tx/<path:tx_hash>")
def tx_route(tx_hash): def tx_route(tx_hash):
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p") return _render_index("tx", tx_hash)
return render_template("index.html", datetime=current_datetime)
@app.route("/block/<path:block_id>") @app.route("/block/<path:block_id>")
def block_route(block_id): def block_route(block_id):
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p") return _render_index("block", block_id)
return render_template("index.html", datetime=current_datetime)
@app.route("/header/<path:block_id>") @app.route("/header/<path:block_id>")
def header_route(block_id): def header_route(block_id):
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p") return _render_index("header", block_id)
return render_template("index.html", datetime=current_datetime)
@app.route("/address/<path:address>") @app.route("/address/<path:address>")
def address_route(address): def address_route(address):
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p") return _render_index("address", address)
return render_template("index.html", datetime=current_datetime)
@app.route("/name/<path:name>") @app.route("/name/<path:name>")
def name_route(name): def name_route(name):
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p") return _render_index("name", name)
return render_template("index.html", datetime=current_datetime)
@app.route("/coin/<path:coin_hash>/<int:index>")
def coin_route(coin_hash, index):
return _render_index("coin", f"{coin_hash}:{index}")
@app.route("/og-image")
def og_image():
context = _get_og_image_context()
title = context["title"]
subtitle = context["subtitle"]
search_type = context["search_type"]
display_value = context["display_value"]
value_font_size = context["value_font_size"]
is_default_card = context["is_default_card"]
type_text = escape(search_type)
value_text = escape(display_value)
title_text = escape(title)
subtitle_text = escape(subtitle)
if is_default_card:
svg = f"""<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\" viewBox=\"0 0 1200 630\">
<defs>
<linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">
<stop offset=\"0%\" stop-color=\"#040b1a\" />
<stop offset=\"100%\" stop-color=\"#160a24\" />
</linearGradient>
<linearGradient id=\"accent\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">
<stop offset=\"0%\" stop-color=\"#cd408f\" />
<stop offset=\"100%\" stop-color=\"#0c4fc2\" />
</linearGradient>
</defs>
<rect width=\"1200\" height=\"630\" fill=\"url(#bg)\" />
<circle cx=\"1100\" cy=\"80\" r=\"180\" fill=\"#08398c\" opacity=\"0.12\" />
<circle cx=\"120\" cy=\"610\" r=\"240\" fill=\"#8e2d63\" opacity=\"0.11\" />
<rect x=\"86\" y=\"74\" width=\"1028\" height=\"482\" rx=\"28\" fill=\"#0b1426\" stroke=\"#64748b\" />
<rect x=\"130\" y=\"126\" width=\"176\" height=\"42\" rx=\"21\" fill=\"url(#accent)\" />
<text x=\"218\" y=\"153\" text-anchor=\"middle\" fill=\"#ffffff\" font-size=\"19\" font-family=\"Inter, Arial, sans-serif\" font-weight=\"700\">Handshake</text>
<text x=\"130\" y=\"246\" fill=\"#ffffff\" font-size=\"82\" font-family=\"Inter, Arial, sans-serif\" font-weight=\"800\">{title_text}</text>
<text x=\"130\" y=\"302\" fill=\"#e5e7eb\" font-size=\"32\" font-family=\"Inter, Arial, sans-serif\">{subtitle_text}</text>
<line x1=\"130\" y1=\"338\" x2=\"1070\" y2=\"338\" stroke=\"#64748b\" />
<text x=\"130\" y=\"402\" fill=\"#f9fafb\" font-size=\"34\" font-family=\"Inter, Arial, sans-serif\" font-weight=\"600\">Blocks • Transactions • Addresses • Names</text>
<text x=\"130\" y=\"452\" fill=\"#cbd5e1\" font-size=\"28\" font-family=\"Inter, Arial, sans-serif\">Real-time status, searchable history, and rich on-chain data.</text>
<text x=\"86\" y=\"608\" fill=\"#cbd5e1\" font-size=\"24\" font-family=\"Inter, Arial, sans-serif\">explorer.hns.au</text>
</svg>"""
else:
svg = f"""<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\" viewBox=\"0 0 1200 630\">
<defs>
<linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">
<stop offset=\"0%\" stop-color=\"#040b1a\" />
<stop offset=\"100%\" stop-color=\"#160a24\" />
</linearGradient>
<linearGradient id=\"accent\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">
<stop offset=\"0%\" stop-color=\"#cd408f\" />
<stop offset=\"100%\" stop-color=\"#0c4fc2\" />
</linearGradient>
<clipPath id=\"valueClip\">
<rect x=\"96\" y=\"496\" width=\"1008\" height=\"52\" rx=\"4\" />
</clipPath>
</defs>
<rect width=\"1200\" height=\"630\" fill=\"url(#bg)\" />
<circle cx=\"1040\" cy=\"120\" r=\"180\" fill=\"#08398c\" opacity=\"0.11\" />
<circle cx=\"160\" cy=\"580\" r=\"220\" fill=\"#8e2d63\" opacity=\"0.10\" />
<rect x=\"72\" y=\"64\" width=\"240\" height=\"44\" rx=\"22\" fill=\"url(#accent)\" />
<text x=\"192\" y=\"92\" text-anchor=\"middle\" fill=\"#ffffff\" font-size=\"20\" font-family=\"Inter, Arial, sans-serif\" font-weight=\"700\">{type_text}</text>
<text x=\"72\" y=\"188\" fill=\"#ffffff\" font-size=\"62\" font-family=\"Inter, Arial, sans-serif\" font-weight=\"700\">{title_text}</text>
<text x=\"72\" y=\"252\" fill=\"#e5e7eb\" font-size=\"33\" font-family=\"Inter, Arial, sans-serif\">{subtitle_text}</text>
<rect x=\"72\" y=\"472\" width=\"1056\" height=\"96\" rx=\"16\" fill=\"#0b1426\" stroke=\"#64748b\" />
<text x=\"96\" y=\"530\" clip-path=\"url(#valueClip)\" fill=\"#f9fafb\" font-size=\"{value_font_size}\" font-family=\"'JetBrains Mono', 'Consolas', monospace\">{value_text}</text>
<text x=\"72\" y=\"610\" fill=\"#cbd5e1\" font-size=\"24\" font-family=\"Inter, Arial, sans-serif\">explorer.hns.au</text>
</svg>"""
response = make_response(svg)
response.headers["Content-Type"] = "image/svg+xml; charset=utf-8"
response.headers["Cache-Control"] = "public, max-age=300"
return response
@app.route("/og-image.png")
def og_image_png():
try:
image_module = importlib.import_module("PIL.Image")
image_draw_module = importlib.import_module("PIL.ImageDraw")
except Exception:
if os.path.isfile("templates/assets/img/og.png"):
return send_from_directory("templates/assets/img", "og.png")
return og_image()
context = _get_og_image_context()
title = context["title"]
subtitle = context["subtitle"]
search_type = context["search_type"]
display_value = context["display_value"]
is_default_card = context["is_default_card"]
width, height = 1200, 630
image = image_module.new("RGBA", (width, height), "#040b1a")
draw = image_draw_module.Draw(image, "RGBA")
draw.rectangle((0, 0, width, height), fill="#040b1a")
draw.ellipse((920, -60, 1280, 300), fill=(8, 57, 140, 30))
draw.ellipse((-120, 360, 360, 840), fill=(142, 45, 99, 32))
title_font = _load_og_font(56, bold=True)
subtitle_font = _load_og_font(33, bold=False)
label_font = _load_og_font(20, bold=True)
mono_font = _load_og_font(30, bold=False, mono=True)
footer_font = _load_og_font(24, bold=False)
if is_default_card:
draw.rounded_rectangle(
(86, 74, 1114, 556), radius=28, fill="#0b1426", outline="#64748b", width=2
)
draw.rounded_rectangle((130, 126, 306, 168), radius=21, fill="#cd408f")
_draw_text_shadow(draw, (165, 133), "Handshake", label_font, "#ffffff")
nice_title_font = _load_og_font(74, bold=True)
nice_subtitle_font = _load_og_font(30, bold=False)
features_font = _load_og_font(34, bold=True)
subfeatures_font = _load_og_font(28, bold=False)
title_lines = _wrap_text(draw, title, nice_title_font, 940, max_lines=2)
subtitle_lines = _wrap_text(
draw, subtitle, nice_subtitle_font, 940, max_lines=2
)
title_y = 192
for line in title_lines:
_draw_text_shadow(draw, (130, title_y), line, nice_title_font, "#ffffff")
title_y += 76
subtitle_y = title_y + 8
for line in subtitle_lines:
_draw_text_shadow(
draw, (130, subtitle_y), line, nice_subtitle_font, "#e5e7eb"
)
subtitle_y += 38
divider_y = subtitle_y + 18
draw.line((130, divider_y, 1070, divider_y), fill="#64748b", width=2)
features_y = divider_y + 30
_draw_text_shadow(
draw,
(130, features_y),
"Blocks • Transactions • Addresses • Names",
features_font,
"#f9fafb",
)
_draw_text_shadow(
draw,
(130, features_y + 52),
"Real-time status, searchable history, and rich on-chain data.",
subfeatures_font,
"#cbd5e1",
)
_draw_text_shadow(draw, (86, 580), "explorer.hns.au", footer_font, "#cbd5e1")
else:
draw.rounded_rectangle((72, 64, 312, 108), radius=22, fill="#cd408f")
safe_type = _fit_text(draw, search_type, label_font, 220)
type_width = draw.textbbox((0, 0), safe_type, font=label_font)[2]
_draw_text_shadow(
draw, (192 - type_width // 2, 72), safe_type, label_font, "#ffffff"
)
title_lines = _wrap_text(draw, title, title_font, 1040, max_lines=2)
subtitle_lines = _wrap_text(draw, subtitle, subtitle_font, 1040, max_lines=2)
title_y = 132
for line in title_lines:
_draw_text_shadow(draw, (72, title_y), line, title_font, "#ffffff")
title_y += 66
subtitle_y = title_y + 8
for line in subtitle_lines:
_draw_text_shadow(draw, (72, subtitle_y), line, subtitle_font, "#e5e7eb")
subtitle_y += 38
draw.rounded_rectangle(
(72, 472, 1128, 568), radius=16, fill="#0b1426", outline="#64748b", width=2
)
value_max_px = 1008
safe_value = _fit_text(draw, display_value, mono_font, value_max_px)
_draw_text_shadow(draw, (96, 500), safe_value, mono_font, "#f9fafb")
_draw_text_shadow(draw, (72, 580), "explorer.hns.au", footer_font, "#cbd5e1")
output = BytesIO()
image.convert("RGB").save(output, format="PNG", optimize=True)
output.seek(0)
response = send_file(output, mimetype="image/png")
response.headers["Cache-Control"] = "public, max-age=300"
return response
@app.route("/<path:path>") @app.route("/<path:path>")
@@ -128,6 +799,184 @@ def catch_all(path: str):
# endregion # endregion
# region API routes
@app.route("/api/v1/namehash/<namehash>")
def namehash_api(namehash):
db = get_db()
cur = db.execute("SELECT * FROM names WHERE namehash = ?", (namehash,))
row = cur.fetchone()
if row is None:
# Get namehash from hsd.hns.au
req = requests.get(f"https://hsd.hns.au/api/v1/namehash/{namehash}")
if req.status_code == 200:
name = req.json().get("result")
if not name:
return jsonify({"name": "Error", "namehash": namehash})
# Insert into database
db.execute(
"INSERT OR REPLACE INTO names (namehash, name) VALUES (?, ?)",
(namehash, name),
)
db.commit()
return jsonify({"name": name, "namehash": namehash})
return jsonify(dict(row))
@app.route("/api/v1/status")
def api_status():
# Count number of names in database
db = get_db()
cur = db.execute("SELECT COUNT(*) as count FROM names")
row = cur.fetchone()
name_count = row["count"] if row else 0
return jsonify(
{
"status": "ok",
"service": "FireExplorer",
"version": "1.0.0",
"names_cached": name_count,
}
)
@app.route("/api/v1/hip02/<domain>")
def hip02(domain: str):
hip2_record = hip2(domain)
if hip2_record:
return jsonify(
{
"success": True,
"address": hip2_record,
"method": "hip02",
"name": domain,
}
)
wallet_record = wallet_txt(domain)
if wallet_record:
return jsonify(
{
"success": True,
"address": wallet_record,
"method": "wallet_txt",
"name": domain,
}
)
return jsonify(
{
"success": False,
"name": domain,
"error": "No HIP02 or WALLET record found for this domain",
}
)
@app.route("/api/v1/covenant", methods=["POST"])
def covenant_api():
data = request.get_json()
if isinstance(data, list):
covenants = data
results = []
# Collect all namehashes needed
namehashes = set()
for cov in covenants:
items = cov.get("items", [])
if items:
namehashes.add(items[0])
# Batch DB lookup
db = get_db()
known_names = {}
if namehashes:
placeholders = ",".join("?" for _ in namehashes)
cur = db.execute(
f"SELECT namehash, name FROM names WHERE namehash IN ({placeholders})",
list(namehashes),
)
for row in cur:
known_names[row["namehash"]] = row["name"]
# Identify missing namehashes
missing_hashes = [nh for nh in namehashes if nh not in known_names]
# Fetch missing from HSD
session = requests.Session()
for nh in missing_hashes:
try:
req = session.get(f"https://hsd.hns.au/api/v1/namehash/{nh}")
if req.status_code == 200:
name = req.json().get("result")
if name:
known_names[nh] = name
# Update DB
db.execute(
"INSERT OR REPLACE INTO names (namehash, name) VALUES (?, ?)",
(nh, name),
)
except Exception as e:
print(f"Error fetching namehash {nh}: {e}")
db.commit()
# Build results
for cov in covenants:
action = cov.get("action")
items = cov.get("items", [])
if not action:
results.append({"covenant": cov, "display": "Unknown"})
continue
display = f"{action}"
if items:
nh = items[0]
if nh in known_names:
name = known_names[nh]
display += f' <a href="/name/{name}">{name}</a>'
results.append({"covenant": cov, "display": display})
return jsonify(results)
# Get the covenant data
action = data.get("action")
items = data.get("items", [])
if not action:
return jsonify({"success": False, "data": data})
display = f"{action}"
if len(items) > 0:
name_hash = items[0]
# Lookup name from database
db = get_db()
cur = db.execute("SELECT * FROM names WHERE namehash = ?", (name_hash,))
row = cur.fetchone()
if row:
name = row["name"]
display += f' <a href="/name/{name}">{name}</a>'
else:
req = requests.get(f"https://hsd.hns.au/api/v1/namehash/{name_hash}")
if req.status_code == 200:
name = req.json().get("result")
if name:
display += f" {name}"
# Insert into database
db.execute(
"INSERT OR REPLACE INTO names (namehash, name) VALUES (?, ?)",
(name_hash, name),
)
db.commit()
return jsonify({"success": True, "data": data, "display": display})
# endregion
# region Error Catching # region Error Catching
# 404 catch all # 404 catch all
@app.errorhandler(404) @app.errorhandler(404)

View File

@@ -6,6 +6,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nathan.Woodburn/</title> <title>Nathan.Woodburn/</title>
<link rel="icon" href="/assets/img/favicon.png" type="image/png"> <link rel="icon" href="/assets/img/favicon.png" type="image/png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/css/404.css"> <link rel="stylesheet" href="/assets/css/404.css">
</head> </head>

View File

@@ -1,20 +1,45 @@
:root {
--primary-gradient: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 50%, #ec4899 100%);
--bg-color: #0f172a;
--text-primary: #f8fafc;
--accent-color: #8b5cf6;
}
body { body {
background-color: #000000; background-color: var(--bg-color);
color: #ffffff; background-image:
} radial-gradient(at 0% 0%, rgba(59, 130, 246, 0.15) 0px, transparent 50%),
h1 { radial-gradient(at 100% 0%, rgba(236, 72, 153, 0.15) 0px, transparent 50%);
font-size: 50px; color: var(--text-primary);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0; margin: 0;
padding: 0; min-height: 100vh;
display: flex;
flex-direction: column;
} }
h1 {
font-size: 3rem;
margin: 0 0 1rem 0;
background: var(--primary-gradient);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.centre { .centre {
margin-top: 10%; margin: auto;
text-align: center; text-align: center;
padding: 2rem;
} }
a { a {
color: #ffffff; color: var(--accent-color);
text-decoration: none; text-decoration: none;
transition: color 0.2s ease;
} }
a:hover { a:hover {
color: #a78bfa;
text-decoration: underline; text-decoration: underline;
} }

View File

@@ -1,3 +1,15 @@
:root {
--primary-gradient: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 50%, #ec4899 100%);
--bg-color: #0f172a;
--card-bg: rgba(30, 41, 59, 0.7);
--card-border: rgba(148, 163, 184, 0.1);
--text-primary: #f8fafc;
--text-secondary: #94a3b8;
--accent-color: #8b5cf6;
--success-color: #10b981;
--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
@@ -5,9 +17,12 @@
} }
body { body {
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%); background-color: var(--bg-color);
color: #e0e0e0; background-image:
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; radial-gradient(at 0% 0%, rgba(59, 130, 246, 0.15) 0px, transparent 50%),
radial-gradient(at 100% 0%, rgba(236, 72, 153, 0.15) 0px, transparent 50%);
color: var(--text-primary);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6; line-height: 1.6;
min-height: 100vh; min-height: 100vh;
} }
@@ -20,24 +35,56 @@ body {
/* Header */ /* Header */
header { header {
background: linear-gradient(135deg, #8b2f0a 0%, #6b3d0a 100%); background: rgba(15, 23, 42, 0.8);
padding: 2rem 0; backdrop-filter: blur(12px);
text-align: center; -webkit-backdrop-filter: blur(12px);
box-shadow: 0 4px 20px rgba(255, 107, 53, 0.3); padding: 1.5rem 0;
margin: 0 0 2rem 0; position: sticky;
top: 0;
z-index: 100;
border-bottom: 1px solid var(--card-border);
margin-bottom: 2rem;
}
header .container {
display: flex;
align-items: center;
justify-content: space-between;
}
.brand {
display: flex;
align-items: center;
gap: 1rem;
} }
header h1 { header h1 {
font-size: 3rem; font-size: 1.8rem;
font-weight: 700;
background: var(--primary-gradient);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
margin: 0; margin: 0;
color: #ffffff; display: flex;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); align-items: center;
gap: 0.5rem;
}
header a:hover {
text-decoration: none;
} }
.subtitle { .subtitle {
color: rgba(255, 255, 255, 0.9); color: var(--text-secondary);
font-size: 1.2rem; font-size: 0.9rem;
margin-top: 0.5rem; font-weight: 500;
display: none; /* Hidden on mobile, shown on desktop */
}
@media (min-width: 768px) {
.subtitle {
display: block;
}
} }
/* Main Content */ /* Main Content */
@@ -45,26 +92,47 @@ main {
padding: 2rem 0; padding: 2rem 0;
} }
section {
scroll-margin-top: 140px;
}
/* Cards */ /* Cards */
.card { .card {
background: rgba(30, 30, 30, 0.8); background: var(--card-bg);
border-radius: 12px; backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: 16px;
padding: 2rem; padding: 2rem;
margin-bottom: 2rem; margin-bottom: 2rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); box-shadow: var(--glass-shadow);
border: 1px solid rgba(255, 107, 53, 0.2); border: 1px solid var(--card-border);
transition: transform 0.2s ease, box-shadow 0.2s ease;
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
opacity: 0; /* Start hidden for animation */
min-width: 0; /* Prevent grid overflow */
}
.card:hover {
border-color: rgba(139, 92, 246, 0.3);
} }
.card h2 { .card h2 {
color: #ff6b35; color: var(--text-primary);
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
font-size: 1.8rem; font-size: 1.5rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
} }
.card h3 { .card h3 {
color: #f7931e; color: var(--text-secondary);
margin-bottom: 1rem; margin-bottom: 1rem;
font-size: 1.3rem; font-size: 1.1rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
} }
/* Status Section */ /* Status Section */
@@ -77,25 +145,31 @@ main {
.status-card { .status-card {
padding: 1.5rem; padding: 1.5rem;
display: flex;
flex-direction: column;
height: 100%;
} }
.status-content { .status-content {
color: #b0b0b0; color: var(--text-primary);
font-size: 0.9rem; font-size: 1.1rem;
font-weight: 500;
margin-top: auto;
} }
/* Info Grid */ /* Info Grid */
.info-grid { .info-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.75rem; gap: 1rem;
} }
.info-item { .info-item {
padding: 0.5rem; padding: 1rem;
background: rgba(20, 20, 20, 0.5); background: rgba(15, 23, 42, 0.4);
border-radius: 6px; border-radius: 12px;
border: 1px solid rgba(255, 107, 53, 0.15); border: 1px solid var(--card-border);
min-width: 0; /* Prevent grid overflow */
} }
.info-item.no-border { .info-item.no-border {
@@ -109,86 +183,127 @@ main {
} }
.info-item strong { .info-item strong {
color: #ff6b35; color: var(--text-secondary);
display: inline-block; display: block;
min-width: 100px; font-size: 0.8rem;
margin-bottom: 0.25rem;
text-transform: uppercase;
letter-spacing: 0.05em;
} }
.mono { .mono {
font-family: 'Courier New', monospace; font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.85rem; font-size: 0.9rem;
word-break: break-all; word-break: break-all;
color: var(--text-primary);
} }
.view-all-btn { .view-all-btn {
display: inline-block; display: inline-flex;
padding: 0.5rem 1rem; align-items: center;
background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%); justify-content: center;
padding: 0.6rem 1.2rem;
background: var(--primary-gradient);
color: #ffffff !important; color: #ffffff !important;
text-decoration: none !important; text-decoration: none !important;
border-radius: 6px; border-radius: 8px;
font-weight: 600; font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s ease; transition: all 0.3s ease;
border: none;
cursor: pointer;
} }
.view-all-btn:hover { .view-all-btn:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 107, 53, 0.4); box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4);
} }
/* Mempool Transaction List */ /* Mempool Transaction List */
.mempool-txs-container { .mempool-txs-container {
margin-top: 1rem; margin-top: 1.5rem;
} }
.mempool-header { .mempool-header {
padding: 0.75rem; padding: 0.75rem;
background: rgba(255, 107, 53, 0.1); background: rgba(139, 92, 246, 0.1);
border-radius: 6px; border-radius: 8px;
margin-bottom: 0.75rem; margin-bottom: 1rem;
color: #ff6b35; color: var(--accent-color);
font-weight: 600;
font-size: 0.9rem;
} }
.tx-list { .tx-list {
max-height: 400px; max-height: 400px;
overflow-y: auto; overflow-y: auto;
padding-right: 0.5rem;
}
/* Custom Scrollbar */
.tx-list::-webkit-scrollbar {
width: 6px;
}
.tx-list::-webkit-scrollbar-track {
background: rgba(15, 23, 42, 0.3);
border-radius: 3px;
}
.tx-list::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.3);
border-radius: 3px;
} }
.tx-item { .tx-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 1rem;
padding: 0.5rem; padding: 0.75rem;
background: rgba(20, 20, 20, 0.5); background: rgba(15, 23, 42, 0.4);
border: 1px solid rgba(255, 107, 53, 0.15); border: 1px solid var(--card-border);
border-radius: 6px; border-radius: 8px;
margin-bottom: 0.5rem; margin-bottom: 0.75rem;
transition: all 0.2s ease;
animation: staggerFade 0.4s ease forwards;
cursor: pointer;
min-width: 0; /* Prevent flex overflow */
}
.tx-item:hover {
border-color: rgba(139, 92, 246, 0.3);
background: rgba(139, 92, 246, 0.05);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
} }
.tx-hash { .tx-hash {
flex: 1; flex: 1;
font-size: 0.8rem; font-family: 'JetBrains Mono', monospace;
color: #b0b0b0; font-size: 0.85rem;
color: var(--text-secondary);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap;
min-width: 0; /* Prevent flex overflow */
} }
.tx-view-btn { .tx-view-btn {
padding: 0.4rem 0.8rem; padding: 0.4rem 0.8rem;
background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%); background: rgba(139, 92, 246, 0.1);
color: #ffffff; color: var(--accent-color);
border: none; border: 1px solid rgba(139, 92, 246, 0.2);
border-radius: 4px; border-radius: 6px;
cursor: pointer; cursor: pointer;
font-size: 0.85rem; font-size: 0.8rem;
font-weight: 600; font-weight: 600;
transition: all 0.3s ease; transition: all 0.2s ease;
flex-shrink: 0; flex-shrink: 0;
} }
.tx-view-btn:hover { .tx-view-btn:hover {
transform: translateY(-1px); background: var(--accent-color);
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.4); color: white;
} }
/* Transaction Modal */ /* Transaction Modal */
@@ -198,23 +313,39 @@ main {
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: rgba(0, 0, 0, 0.8); background: rgba(15, 23, 42, 0.9);
backdrop-filter: blur(8px);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 1000; z-index: 1000;
padding: 1rem; padding: 1rem;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.tx-modal.active {
opacity: 1;
pointer-events: auto;
} }
.tx-modal-content { .tx-modal-content {
background: rgba(30, 30, 30, 0.95); background: #1e293b;
border-radius: 12px; border-radius: 16px;
max-width: 900px; max-width: 900px;
width: 100%; width: 100%;
max-height: 80vh; max-height: 85vh;
border: 1px solid rgba(255, 107, 53, 0.3); border: 1px solid var(--card-border);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
transform: translateY(20px);
transition: transform 0.3s ease;
}
.tx-modal.active .tx-modal-content {
transform: translateY(0);
} }
.tx-modal-header { .tx-modal-header {
@@ -222,33 +353,30 @@ main {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 1.5rem; padding: 1.5rem;
border-bottom: 1px solid rgba(255, 107, 53, 0.2); border-bottom: 1px solid var(--card-border);
} }
.tx-modal-header h3 { .tx-modal-header h3 {
margin: 0; margin: 0;
color: #ff6b35; color: var(--text-primary);
font-size: 1.2rem;
} }
.tx-modal-close { .tx-modal-close {
background: none; background: transparent;
border: none; border: none;
color: #e0e0e0; color: var(--text-secondary);
font-size: 2rem; font-size: 1.5rem;
cursor: pointer; cursor: pointer;
padding: 0; padding: 0.5rem;
width: 2rem; border-radius: 8px;
height: 2rem; transition: all 0.2s ease;
display: flex; line-height: 1;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.3s ease;
} }
.tx-modal-close:hover { .tx-modal-close:hover {
background: rgba(255, 107, 53, 0.2); background: rgba(255, 255, 255, 0.1);
color: #ff6b35; color: var(--text-primary);
} }
.tx-modal-body { .tx-modal-body {
@@ -258,80 +386,90 @@ main {
} }
.tx-modal-body pre { .tx-modal-body pre {
color: #b0b0b0; color: var(--text-secondary);
font-family: 'Courier New', monospace; font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem; font-size: 0.9rem;
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
margin: 0; margin: 0;
word-break: break-all;
} }
/* Transaction Details */ /* Transaction Details */
.tx-details { .tx-details {
color: #e0e0e0; color: var(--text-primary);
} }
.tx-section { .tx-section {
margin-bottom: 1.5rem; margin-bottom: 2rem;
} }
.tx-section h4 { .tx-section h4 {
color: #ff6b35; color: var(--accent-color);
margin-bottom: 1rem; margin-bottom: 1rem;
font-size: 1.1rem; font-size: 1rem;
text-transform: uppercase;
letter-spacing: 0.05em;
} }
.tx-io-list { .tx-io-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.75rem; gap: 1rem;
} }
.tx-io-item { .tx-io-item {
background: rgba(20, 20, 20, 0.5); background: rgba(15, 23, 42, 0.4);
border: 1px solid rgba(255, 107, 53, 0.15); border: 1px solid var(--card-border);
border-radius: 6px; border-radius: 8px;
padding: 0.75rem; padding: 1rem;
min-width: 0; /* Prevent overflow */
} }
.tx-io-header { .tx-io-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 0.5rem; margin-bottom: 0.75rem;
} }
.tx-io-index { .tx-io-index {
color: #ff6b35; color: var(--text-secondary);
font-weight: 600; font-size: 0.85rem;
font-size: 0.9rem; background: rgba(255, 255, 255, 0.05);
padding: 0.2rem 0.5rem;
border-radius: 4px;
} }
.tx-io-value { .tx-io-value {
color: #f7931e; color: var(--success-color);
font-weight: 600; font-weight: 600;
font-size: 1rem; font-size: 1rem;
font-family: 'JetBrains Mono', monospace;
} }
.tx-io-address { .tx-io-address {
color: #b0b0b0; color: var(--text-primary);
font-family: 'Courier New', monospace; font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem; font-size: 0.9rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
word-break: break-all; word-break: break-all;
} }
.tx-io-hash { .tx-io-hash {
color: #808080; color: var(--text-secondary);
font-size: 0.75rem; font-size: 0.8rem;
word-break: break-all; word-break: break-all;
font-family: 'JetBrains Mono', monospace;
} }
.tx-covenant { .tx-covenant {
color: #ff6b35; color: var(--accent-color);
font-size: 0.85rem; font-size: 0.85rem;
margin-top: 0.5rem; margin-top: 0.75rem;
font-style: italic; padding-top: 0.75rem;
border-top: 1px solid var(--card-border);
font-family: 'JetBrains Mono', monospace;
} }
/* Tabs */ /* Tabs */
@@ -340,38 +478,53 @@ main {
gap: 0.5rem; gap: 0.5rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
flex-wrap: wrap; flex-wrap: wrap;
background: rgba(15, 23, 42, 0.4);
padding: 0.5rem;
border-radius: 12px;
border: 1px solid var(--card-border);
} }
.tab-btn { .tab-btn {
background: rgba(50, 50, 50, 0.5); flex: 1;
color: #e0e0e0; background: transparent;
border: 1px solid rgba(255, 107, 53, 0.3); color: var(--text-secondary);
padding: 0.75rem 1.5rem; border: none;
padding: 0.75rem 1rem;
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
font-size: 1rem; font-size: 0.95rem;
transition: all 0.3s ease; font-weight: 500;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
} }
.tab-btn:hover { .tab-btn:hover {
background: rgba(255, 107, 53, 0.2); color: var(--text-primary);
border-color: #ff6b35; background: rgba(255, 255, 255, 0.08);
} }
.tab-btn.active { .tab-btn.active {
background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%); background: var(--primary-gradient);
color: #ffffff; color: #ffffff;
border-color: #ff6b35; box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
transform: translateY(-1px);
} }
.tab-content { .tab-content {
display: none; display: none;
animation: fadeIn 0.3s ease;
} }
.tab-content.active { .tab-content.active {
display: block; display: block;
} }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Search Box */ /* Search Box */
.search-box { .search-box {
display: flex; display: flex;
@@ -383,39 +536,42 @@ main {
.search-box input { .search-box input {
flex: 1; flex: 1;
min-width: 200px; min-width: 200px;
padding: 0.75rem 1rem; padding: 0.8rem 1.2rem;
background: rgba(20, 20, 20, 0.8); background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(255, 107, 53, 0.3); border: 1px solid var(--card-border);
border-radius: 8px; border-radius: 10px;
color: #e0e0e0; color: var(--text-primary);
font-size: 1rem; font-size: 1rem;
transition: all 0.2s ease;
} }
.search-box input:focus { .search-box input:focus {
outline: none; outline: none;
border-color: #ff6b35; border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(255, 107, 53, 0.2); box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.15), 0 0 20px rgba(139, 92, 246, 0.2);
background: rgba(15, 23, 42, 0.9);
} }
.search-box input::placeholder { .search-box input::placeholder {
color: rgba(224, 224, 224, 0.5); color: var(--text-secondary);
} }
.search-box button { .search-box button {
padding: 0.75rem 1.5rem; padding: 0.8rem 1.5rem;
background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%); background: var(--primary-gradient);
color: #ffffff; color: #ffffff;
border: none; border: none;
border-radius: 8px; border-radius: 10px;
cursor: pointer; cursor: pointer;
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
transition: all 0.3s ease; transition: all 0.3s ease;
white-space: nowrap;
} }
.search-box button:hover { .search-box button:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 107, 53, 0.4); box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4);
} }
.search-box button:active { .search-box button:active {
@@ -423,30 +579,32 @@ main {
} }
.secondary-btn { .secondary-btn {
padding: 0.75rem 1.5rem; padding: 0.8rem 1.5rem;
background: rgba(50, 50, 50, 0.8); background: transparent;
color: #ff6b35; color: var(--text-primary);
border: 1px solid #ff6b35; border: 1px solid var(--card-border);
border-radius: 8px; border-radius: 10px;
cursor: pointer; cursor: pointer;
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
transition: all 0.3s ease; transition: all 0.2s ease;
} }
.secondary-btn:hover { .secondary-btn:hover {
background: rgba(255, 107, 53, 0.2); background: rgba(255, 255, 255, 0.05);
border-color: var(--text-secondary);
} }
/* Result Box */ /* Result Box */
.result-box { .result-box {
background: rgba(10, 10, 10, 0.8); background: rgba(15, 23, 42, 0.4);
border: 1px solid rgba(255, 107, 53, 0.2); border: 1px solid var(--card-border);
border-radius: 8px; border-radius: 12px;
padding: 1rem; padding: 1.5rem;
min-height: 100px; min-height: 100px;
max-height: 600px; max-height: 600px;
overflow-y: auto; overflow-y: auto;
margin-top: 1rem;
} }
.result-box:empty { .result-box:empty {
@@ -454,16 +612,31 @@ main {
} }
.result-box pre { .result-box pre {
color: #b0b0b0; color: var(--text-secondary);
font-family: 'Courier New', monospace; font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem; font-size: 0.9rem;
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
word-break: break-all;
} }
.result-box .error { .result-box .error {
color: #ff4444; color: #ef4444;
font-weight: 600; font-weight: 600;
background: rgba(239, 68, 68, 0.1);
padding: 1rem;
border-radius: 8px;
border: 1px solid rgba(239, 68, 68, 0.2);
}
.success-message {
color: var(--success-color);
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.2);
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
word-break: break-all;
} }
/* Scrollbar */ /* Scrollbar */
@@ -472,58 +645,55 @@ main {
} }
.result-box::-webkit-scrollbar-track { .result-box::-webkit-scrollbar-track {
background: rgba(20, 20, 20, 0.5); background: rgba(15, 23, 42, 0.3);
border-radius: 4px; border-radius: 4px;
} }
.result-box::-webkit-scrollbar-thumb { .result-box::-webkit-scrollbar-thumb {
background: rgba(255, 107, 53, 0.5); background: rgba(148, 163, 184, 0.3);
border-radius: 4px; border-radius: 4px;
} }
.result-box::-webkit-scrollbar-thumb:hover { .result-box::-webkit-scrollbar-thumb:hover {
background: rgba(255, 107, 53, 0.7); background: rgba(148, 163, 184, 0.5);
} }
/* Footer */ /* Footer */
footer { footer {
background: rgba(20, 20, 20, 0.8); background: rgba(15, 23, 42, 0.8);
backdrop-filter: blur(12px);
padding: 2rem 0; padding: 2rem 0;
text-align: center; text-align: center;
margin-top: 3rem; margin-top: 4rem;
border-top: 1px solid rgba(255, 107, 53, 0.2); border-top: 1px solid var(--card-border);
} }
footer p { footer p {
color: rgba(224, 224, 224, 0.7); color: var(--text-secondary);
margin: 0.5rem 0; margin: 0.5rem 0;
} }
.timestamp { .timestamp {
font-size: 0.9rem; font-size: 0.85rem;
color: rgba(224, 224, 224, 0.5); opacity: 0.7;
} }
/* Links */ /* Links */
a { a {
color: #ff6b35; color: var(--accent-color);
text-decoration: none; text-decoration: none;
transition: color 0.3s ease; transition: color 0.2s ease;
} }
a:hover { a:hover {
color: #f7931e; color: #a78bfa;
text-decoration: underline; text-decoration: underline;
} }
/* Responsive Design */ /* Responsive Design */
@media (max-width: 768px) { @media (max-width: 768px) {
header h1 { header h1 {
font-size: 2rem; font-size: 1.5rem;
}
.subtitle {
font-size: 1rem;
} }
.card { .card {
@@ -542,6 +712,31 @@ a:hover {
.status-section { .status-section {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.tab-btn {
padding: 0.5rem;
font-size: 0.85rem;
}
.info-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.card {
padding: 1rem;
}
.container {
padding: 0 1rem;
}
.tx-io-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
} }
/* Loading Animation */ /* Loading Animation */
@@ -557,4 +752,91 @@ a:hover {
.status-content:empty::after { .status-content:empty::after {
content: 'Loading...'; content: 'Loading...';
animation: pulse 1.5s ease-in-out infinite; animation: pulse 1.5s ease-in-out infinite;
color: var(--text-secondary);
font-style: italic;
}
/* Loading Spinner */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
color: var(--text-secondary);
animation: fadeIn 0.3s ease;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(139, 92, 246, 0.1);
border-radius: 50%;
border-top-color: var(--accent-color);
animation: spin 1s cubic-bezier(0.4, 0, 0.2, 1) infinite;
margin-bottom: 1rem;
}
.loading-text {
font-size: 0.9rem;
font-weight: 500;
letter-spacing: 0.05em;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* New Animations */
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes staggerFade {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Apply animations */
.card {
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
opacity: 0; /* Start hidden for animation */
}
.search-section .card {
animation-delay: 0.1s;
}
.status-section .card:nth-child(1) {
animation-delay: 0.2s;
}
.status-section .card:nth-child(2) {
animation-delay: 0.3s;
}
.additional-section .card {
animation-delay: 0.4s;
}
.tx-item {
animation: staggerFade 0.4s ease forwards;
}
small {
color: var(--text-secondary);
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
templates/assets/img/og.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -4,37 +4,44 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fire Explorer - Handshake Blockchain Explorer</title> <title>{{ meta_title or 'Fire Explorer' }}</title>
<link rel="icon" href="/assets/img/favicon.png" type="image/png"> <link rel="icon" href="/assets/img/favicon.png" type="image/png">
<link rel="stylesheet" href="/assets/css/index.css"> <link rel="stylesheet" href="/assets/css/index.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<meta name="description" content="{{ meta_description or 'A hot new Handshake Blockchain Explorer' }}">
<!-- Open Graph Meta Tags -->
<meta property="og:url" content="{{ og_url or 'https://explorer.hns.au/' }}">
<meta property="og:type" content="website">
<meta property="og:title" content="{{ meta_title or 'Fire Explorer' }}">
<meta property="og:description" content="{{ meta_description or 'A hot new Handshake Blockchain Explorer' }}">
<meta property="og:image" content="{{ og_image or 'https://explorer.hns.au/assets/img/og.png' }}">
<meta property="og:image:secure_url" content="{{ og_image or 'https://explorer.hns.au/assets/img/og.png' }}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta property="twitter:domain" content="{{ twitter_domain or 'explorer.hns.au' }}">
<meta property="twitter:url" content="{{ og_url or 'https://explorer.hns.au/' }}">
<meta name="twitter:title" content="{{ meta_title or 'Fire Explorer' }}">
<meta name="twitter:description" content="{{ meta_description or 'A hot new Handshake Blockchain Explorer' }}">
<meta name="twitter:image" content="{{ og_image or 'https://explorer.hns.au/assets/img/og.png' }}">
</head> </head>
<body> <body>
<header> <header>
<div class="container"> <div class="container">
<h1><img src="/assets/img/favicon.png" alt="Fire Icon" style="height: 1em; vertical-align: middle;"> Fire Explorer</h1> <div class="brand">
<p class="subtitle">Handshake Blockchain Explorer</p> <a href="/"><h1><img src="/assets/img/favicon.png" alt="Fire Icon" style="height: 1.2em; vertical-align: middle;"> Fire Explorer</h1></a>
<span class="subtitle">Handshake Blockchain Explorer</span>
</div>
</div> </div>
</header> </header>
<main class="container"> <main class="container">
<!-- Status Cards -->
<section class="status-section">
<div class="card status-card">
<h3>Node Status</h3>
<div id="node-status" class="status-content">Loading...</div>
</div>
<div class="card status-card">
<h3>Chain Info</h3>
<div id="chain-status" class="status-content">Loading...</div>
</div>
<div class="card status-card">
<h3>Mempool</h3>
<div id="mempool-status" class="status-content">Loading...</div>
<div id="mempool-txs" class="mempool-txs-container"></div>
</div>
</section>
<!-- Search Section --> <!-- Search Section -->
<section class="search-section"> <section class="search-section">
<div class="card"> <div class="card">
@@ -72,6 +79,9 @@
<button onclick="searchAddressTx()">Get Transactions</button> <button onclick="searchAddressTx()">Get Transactions</button>
<button onclick="searchAddressCoins()">Get Coins</button> <button onclick="searchAddressCoins()">Get Coins</button>
</div> </div>
<p style="font-size: 0.85rem; color: var(--text-secondary); margin-top: -0.5rem; margin-bottom: 1.5rem; padding-left: 0.5rem;">
<span style="color: var(--accent-color);">Tip:</span> Use <span class="mono" style="color: var(--text-primary);">@name</span> to search via HIP02 alias
</p>
<div id="address-result" class="result-box"></div> <div id="address-result" class="result-box"></div>
</div> </div>
@@ -92,16 +102,16 @@
</div> </div>
</section> </section>
<!-- Coin Lookup --> <!-- Status Cards -->
<section class="additional-section"> <section class="status-section">
<div class="card"> <div class="card status-card">
<h2>Coin Lookup</h2> <h3>Chain Info</h3>
<div class="search-box"> <div id="chain-status" class="status-content">Loading...</div>
<input type="text" id="coin-hash" placeholder="Coin hash" onkeypress="if(event.key === 'Enter') searchCoin()">
<input type="text" id="coin-index" placeholder="Index" onkeypress="if(event.key === 'Enter') searchCoin()">
<button onclick="searchCoin()">Get Coin Info</button>
</div> </div>
<div id="coin-result" class="result-box"></div> <div class="card status-card">
<h3>Mempool</h3>
<div id="mempool-status" class="status-content">Loading...</div>
<div id="mempool-txs" class="mempool-txs-container"></div>
</div> </div>
</section> </section>
</main> </main>
@@ -110,6 +120,7 @@
<div class="container"> <div class="container">
<p>Fire Explorer - Handshake Blockchain Explorer | Powered by <a href="https://hns.au" target="_blank">HNSAU</a> & <a href="https://hsd.hns.au" target="_blank">Fire HSD</a></p> <p>Fire Explorer - Handshake Blockchain Explorer | Powered by <a href="https://hns.au" target="_blank">HNSAU</a> & <a href="https://hsd.hns.au" target="_blank">Fire HSD</a></p>
<p class="timestamp">Last updated: {{ datetime }}</p> <p class="timestamp">Last updated: {{ datetime }}</p>
<small>Note: Date and times are in UTC</small>
</div> </div>
</footer> </footer>
@@ -192,6 +203,38 @@
} }
} }
// Convert unicode/emoji to punycode for Handshake names
function toPunycode(name) {
// Check if name contains non-ASCII characters
if (!/[^\x00-\x7F]/.test(name)) {
return name; // Already ASCII, no conversion needed
}
try {
// Use the browser's built-in URL API to convert to punycode
const url = new URL(`http://${name}`);
return url.hostname;
} catch {
// Fallback: return original if conversion fails
return name;
}
}
// Resolve HIP02 alias
async function resolveHip02(input) {
if (!input.startsWith('@')) return { success: true, address: input };
const name = input.substring(1);
try {
// Try HTTPS first
const response = await fetch(`/api/v1/hip02/${name}`);
const data = await response.json();
return data;
} catch (e) {
return { success: false, error: 'Failed to resolve HIP02 alias. ' + e.message };
}
}
// Format chain data nicely // Format chain data nicely
function formatChainData(chain) { function formatChainData(chain) {
return ` return `
@@ -208,6 +251,13 @@
`; `;
} }
// Open transaction in search tab
function openTx(txId) {
document.getElementById('tx-input').value = txId;
document.querySelector('[data-tab="tx"]').click();
searchTx();
}
// Format mempool data nicely // Format mempool data nicely
function formatMempoolData(mempool) { function formatMempoolData(mempool) {
// Check if mempool is an array of transaction IDs // Check if mempool is an array of transaction IDs
@@ -223,9 +273,8 @@
</div> </div>
<div class="tx-list"> <div class="tx-list">
${mempool.map(txId => ` ${mempool.map(txId => `
<div class="tx-item"> <div class="tx-item" onclick="openTx('${txId}')">
<span class="tx-hash mono">${txId}</span> <span class="tx-hash mono">${txId}</span>
<button class="tx-view-btn" onclick="window.location.href='/tx/${txId}'">View</button>
</div> </div>
`).join('')} `).join('')}
</div> </div>
@@ -346,6 +395,427 @@
return html; return html;
} }
// Format address transactions nicely
function formatAddressTransactions(txs) {
if (!txs || txs.error) {
return `<div class="error">Error: ${txs.error || 'Invalid transaction data'}</div>`;
}
if (!Array.isArray(txs) || txs.length === 0) {
return `<div class="error">No transactions found for this address</div>`;
}
const formatValue = (value) => (value / 1e6).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' HNS';
const formatTime = (timestamp) => new Date(timestamp * 1000).toLocaleString();
let html = `
<div class="tx-details">
<div class="tx-section">
<h4>Transactions (${txs.length})</h4>
<div class="tx-io-list">
${txs.map((tx, i) => {
const totalInput = tx.inputs.reduce((sum, inp) => sum + (inp.coin?.value || 0), 0);
const totalOutput = tx.outputs.reduce((sum, out) => sum + out.value, 0);
return `
<div class="tx-io-item" style="cursor: pointer;" onclick="window.location.href='/tx/${tx.hash}'">
<div class="tx-io-header">
<span class="tx-io-index">${formatTime(tx.time || tx.mtime)}</span>
<span class="tx-io-value">${tx.confirmations.toLocaleString()} confirms</span>
</div>
<div class="tx-io-hash mono">${tx.hash}</div>
<div style="display: flex; justify-content: space-between; margin-top: 0.5rem; font-size: 0.85rem; color: #b0b0b0;">
<span>Block: ${tx.height >= 0 ? tx.height.toLocaleString() : 'Pending'}</span>
<span>Fee: ${formatValue(tx.fee)}</span>
<span>${tx.inputs.length} in → ${tx.outputs.length} out</span>
</div>
</div>
`;
}).join('')}
</div>
</div>
</div>
`;
return html;
}
// Format address coins nicely
function formatAddressCoins(coins) {
if (!coins || coins.error) {
return `<div class="error">Error: ${coins.error || 'Invalid coin data'}</div>`;
}
if (!Array.isArray(coins) || coins.length === 0) {
return `<div class="error">No coins found for this address</div>`;
}
const formatValue = (value) => (value / 1e6).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' HNS';
const totalValue = coins.reduce((sum, coin) => sum + coin.value, 0);
let html = `
<div class="tx-details">
<div class="tx-section">
<h4>Coins (${coins.length}) - Total: ${formatValue(totalValue)}</h4>
<div class="tx-io-list">
${coins.map((coin, i) => `
<div class="tx-io-item">
<div class="tx-io-header">
<span class="tx-io-index">Output #${coin.index}</span>
<span class="tx-io-value">${formatValue(coin.value)}</span>
</div>
<div class="tx-io-address">${coin.address}</div>
<div style="cursor: pointer;" onclick="window.location.href='/tx/${coin.hash}'">
<div class="tx-io-hash mono">${coin.hash}</div>
</div>
<div style="display: flex; justify-content: space-between; margin-top: 0.5rem; font-size: 0.85rem; color: #b0b0b0;">
<span>Height: ${coin.height.toLocaleString()}</span>
<span>Coinbase: ${coin.coinbase ? 'Yes' : 'No'}</span>
<span>Covenant: <span data-covenant-action="${coin.covenant.action}" data-covenant="${encodeURIComponent(JSON.stringify(coin.covenant))}">${coin.covenant.action}</span></span>
</div>
</div>
`).join('')}
</div>
</div>
<div class="tx-section">
<button class="secondary-btn" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">Show Raw JSON</button>
<pre style="display: none;">${JSON.stringify(coins, null, 2)}</pre>
</div>
</div>
`;
return html;
}
// Format individual coin nicely
function formatCoin(coin) {
if (!coin || coin.error) {
return `<div class="error">Error: ${coin.error || 'Invalid coin data'}</div>`;
}
const formatValue = (value) => (value / 1e6).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' HNS';
let html = `
<div class="tx-details">
<div class="tx-section">
<h4>Coin Information</h4>
<div class="info-grid">
<div class="info-item">
<label>Value:</label>
<span>${formatValue(coin.value)}</span>
</div>
<div class="info-item">
<label>Output Index:</label>
<span>${coin.index}</span>
</div>
<div class="info-item">
<label>Height:</label>
<span>${coin.height.toLocaleString()}</span>
</div>
<div class="info-item">
<label>Coinbase:</label>
<span>${coin.coinbase ? 'Yes' : 'No'}</span>
</div>
<div class="info-item">
<label>Version:</label>
<span>${coin.version}</span>
</div>
<div class="info-item">
<label>Covenant:</label>
<span data-covenant-action="${coin.covenant.action}" data-covenant="${encodeURIComponent(JSON.stringify(coin.covenant))}">${coin.covenant.action}</span>
</div>
</div>
</div>
<div class="tx-section">
<h4>Transaction Hash</h4>
<div style="cursor: pointer; padding: 1rem; background: rgba(255, 107, 53, 0.1); border-radius: 8px;" onclick="window.location.href='/tx/${coin.hash}'">
<div class="mono" style="word-break: break-all; color: #ff6b35;">${coin.hash}</div>
</div>
</div>
<div class="tx-section">
<h4>Address</h4>
<div style="cursor: pointer; padding: 1rem; background: rgba(255, 107, 53, 0.1); border-radius: 8px;" onclick="window.location.href='/address/${coin.address}'">
<div class="mono" style="word-break: break-all; color: #ff6b35;">${coin.address}</div>
</div>
</div>
${coin.covenant.items && coin.covenant.items.length > 0 ? `
<div class="tx-section">
<h4>Covenant Items</h4>
<div style="background: rgba(255, 107, 53, 0.05); padding: 1rem; border-radius: 8px;">
${coin.covenant.items.map((item, i) => `
<div style="margin-bottom: 0.5rem;">
<span style="color: #b0b0b0;">Item ${i}:</span>
<span class="mono" style="word-break: break-all;">${item}</span>
</div>
`).join('')}
</div>
</div>
` : ''}
<div class="tx-section">
<button class="secondary-btn" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">Show Raw JSON</button>
<pre style="display: none;">${JSON.stringify(coin, null, 2)}</pre>
</div>
</div>
`;
return html;
}
// Format name info nicely
function formatNameInfo(data) {
if (!data || data.error) {
return `<div class="error">Error: ${data.error || 'Invalid name data'}</div>`;
}
const info = data.info;
const start = data.start;
const formatValue = (value) => (value / 1e6).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' HNS';
const formatDate = (timestamp) => new Date(timestamp * 1000).toLocaleString();
// If name doesn't exist (no info), show error
if (!info) {
return `<div class="error">Name not found</div>`;
}
let html = `
<div class="tx-details">
<div class="tx-section">
<h4>Name: ${info.name}</h4>
<div class="info-grid">
<div class="info-item"><strong>State:</strong> <span style="color: #f7931e;">${info.state}</span></div>
<div class="info-item"><strong>Registered:</strong> ${info.registered ? 'Yes' : 'No'}</div>
<div class="info-item"><strong>Height:</strong> ${info.height.toLocaleString()}</div>
<div class="info-item"><strong>Value:</strong> ${formatValue(info.value)}</div>
<div class="info-item"><strong>Highest Bid:</strong> ${formatValue(info.highest)}</div>
<div class="info-item"><strong>Renewals:</strong> ${info.renewals}</div>
${info.stats.renewalPeriodEnd ? `
<div class="info-item"><strong>Expiry Block:</strong> ${info.stats.renewalPeriodEnd.toLocaleString()}</div>
` : ''}
<div class="info-item"><strong>Expired:</strong> ${info.expired ? 'Yes' : 'No'}</div>
<div class="info-item"><strong>Revoked:</strong> ${info.revoked > 0 ? 'Yes' : 'No'}</div>
<div class="info-item"><strong>Weak:</strong> ${info.weak ? 'Yes' : 'No'}</div>
</div>
</div>
${info.stats ? `
<div class="tx-section">
<h4>Additional Info</h4>
<div class="info-grid">
${Object.entries(info.stats).map(([key, value]) => {
// Format the key from camelCase to Title Case
const formattedKey = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
// Format the value based on type
let formattedValue;
if (typeof value === 'number') {
// Check if it looks like a block number (large integer) or a decimal (days/hours)
if ((Number.isInteger(value) && value > 100) || key.toLowerCase().includes('height') || key.toLowerCase().includes('block')) {
formattedValue = value.toLocaleString();
} else {
formattedValue = value.toFixed(2);
}
} else {
formattedValue = value;
}
return `<div class="info-item"><strong>${formattedKey}:</strong> ${formattedValue}</div>`;
}).join('')}
</div>
</div>
` : ''}
<div class="tx-section">
<h4>Owner</h4>
${ info.owner.hash == '0000000000000000000000000000000000000000000000000000000000000000' ? `
<div style="padding: 1rem; background: rgba(255, 107, 53, 0.1); border-radius: 8px;">
<div class="mono" style="word-break: break-all; color: #ff6b35;">This name is unowned</div>
</div>
` : `<div style="cursor: pointer; padding: 1rem; background: rgba(255, 107, 53, 0.1); border-radius: 8px;" onclick="window.location.href='/tx/${info.owner.hash}'">
<div class="mono" style="word-break: break-all; color: #ff6b35;">TX: ${info.owner.hash}</div>
<div style="color: #b0b0b0; margin-top: 0.5rem;">Output Index: ${info.owner.index}</div>
</div>
`}
</div>
<div class="tx-section">
<h4>Name Hash</h4>
<div class="mono" style="word-break: break-all; padding: 1rem; background: rgba(255, 107, 53, 0.05); border-radius: 8px;">${info.nameHash}</div>
</div>
${start ? `
<div class="tx-section">
<h4>Auction Info</h4>
<div class="info-grid">
<div class="info-item"><strong>Start Height:</strong> ${start.start.toLocaleString()}</div>
<div class="info-item"><strong>Week:</strong> ${start.week}</div>
<div class="info-item"><strong>Reserved:</strong> ${start.reserved ? 'Yes' : 'No'}</div>
<div class="info-item"><strong>Locked:</strong> ${start.locked ? 'Yes' : 'No'}</div>
</div>
</div>
` : ''}
<div class="tx-section">
<button class="secondary-btn" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">Show Raw JSON</button>
<pre style="display: none;">${JSON.stringify(data, null, 2)}</pre>
</div>
</div>
`;
return html;
}
// Format name resource records nicely
function formatNameResource(data) {
if (!data || data.error) {
return `<div class="error">Error: ${data.error || 'Invalid resource data'}</div>`;
}
if (!data.records || data.records.length === 0) {
return `<div class="error">No resource records found</div>`;
}
const records = data.records;
let html = `
<div class="tx-details">
<div class="tx-section">
<h4>DNS Records (${records.length})</h4>
<div class="tx-io-list">
${records.map((record, i) => {
let recordDetails = `<strong>${record.type}</strong>`;
switch(record.type) {
case 'NS':
recordDetails += `${record.ns}`;
break;
case 'DS':
recordDetails += `<br><span style="font-size: 0.9rem;">KeyTag: ${record.keyTag}, Algorithm: ${record.algorithm}, DigestType: ${record.digestType}</span><br><span class="mono" style="font-size: 0.85rem; color: #b0b0b0; word-break: break-all;">${record.digest}</span>`;
break;
case 'TXT':
recordDetails += `<br><span style="color: #b0b0b0;">${record.txt.join('<br>')}</span>`;
break;
case 'GLUE4':
case 'GLUE6':
recordDetails += `${record.ns}<br><span style="color: #f7931e;">${record.address}</span>`;
break;
case 'SYNTH4':
case 'SYNTH6':
recordDetails += ` → <span style="color: #f7931e;">${record.address}</span>`;
break;
}
return `
<div class="tx-io-item">
<div class="tx-io-header">
<span class="tx-io-index">Record #${i}</span>
</div>
<div style="margin-top: 0.5rem;">${recordDetails}</div>
</div>
`;
}).join('')}
</div>
</div>
<div class="tx-section">
<button class="secondary-btn" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">Show Raw JSON</button>
<pre style="display: none;">${JSON.stringify(data, null, 2)}</pre>
</div>
</div>
`;
return html;
}
// Format name summary nicely
function formatNameSummary(data) {
if (!data || data.error) {
return `<div class="error">Error: ${data.error || 'Invalid summary data'}</div>`;
}
const formatValue = (value) => (value).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' HNS';
const formatDate = (timestamp) => new Date(timestamp * 1000).toLocaleString();
let html = `
<div class="tx-details">
<div class="tx-section">
<h4>Name: ${data.name}</h4>
<div class="info-grid">
<div class="info-item"><strong>State:</strong> <span style="color: #f7931e;">${data.state}</span></div>
<div class="info-item"><strong>Height:</strong> ${data.height.toLocaleString()}</div>
<div class="info-item"><strong>Value:</strong> ${formatValue(data.value)}</div>
<div class="info-item"><strong>Blocks Until Expire:</strong> ${data.blocksUntilExpire.toLocaleString()}</div>
<div class="info-item"><strong>Mint Date:</strong> ${formatDate(data.mintTimestamp)}</div>
</div>
</div>
<div class="tx-section">
<h4>Owner Address</h4>
<div style="cursor: pointer; padding: 1rem; background: rgba(255, 107, 53, 0.1); border-radius: 8px;" onclick="window.location.href='/address/${data.owner}'">
<div class="mono" style="word-break: break-all; color: #ff6b35;">${data.owner}</div>
</div>
</div>
<div class="tx-section">
<h4>Name Hash</h4>
<div class="mono" style="word-break: break-all; padding: 1rem; background: rgba(255, 107, 53, 0.05); border-radius: 8px;">${data.hash}</div>
</div>
${data.resources && data.resources[0] && data.resources[0].records ? `
<div class="tx-section">
<h4>DNS Records (${data.resources[0].records.length})</h4>
<div class="tx-io-list">
${data.resources[0].records.map((record, i) => {
let recordDetails = `<strong>${record.type}</strong>`;
switch(record.type) {
case 'NS':
recordDetails += `${record.ns}`;
break;
case 'DS':
recordDetails += `<br><span style="font-size: 0.9rem;">KeyTag: ${record.keyTag}, Algorithm: ${record.algorithm}</span>`;
break;
case 'TXT':
recordDetails += `<br><span style="color: #b0b0b0;">${record.txt.join('<br>')}</span>`;
break;
case 'GLUE4':
case 'GLUE6':
recordDetails += `${record.ns}<br><span style="color: #f7931e;">${record.address}</span>`;
break;
case 'SYNTH4':
case 'SYNTH6':
recordDetails += ` → <span style="color: #f7931e;">${record.address}</span>`;
break;
}
return `
<div class="tx-io-item">
<div class="tx-io-header">
<span class="tx-io-index">Record #${i}</span>
</div>
<div style="margin-top: 0.5rem;">${recordDetails}</div>
</div>
`;
}).join('')}
</div>
</div>
` : ''}
<div class="tx-section">
<button class="secondary-btn" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">Show Raw JSON</button>
<pre style="display: none;">${JSON.stringify(data, null, 2)}</pre>
</div>
</div>
`;
return html;
}
// Format transaction data nicely // Format transaction data nicely
function formatTransactionData(tx) { function formatTransactionData(tx) {
if (!tx || tx.error) { if (!tx || tx.error) {
@@ -372,16 +842,32 @@
<div class="tx-section"> <div class="tx-section">
<h4>Inputs (${tx.inputs.length})</h4> <h4>Inputs (${tx.inputs.length})</h4>
<div class="tx-io-list"> <div class="tx-io-list">
${tx.inputs.map((input, i) => ` ${tx.inputs.map((input, i) => {
// Check if this is a coinbase input
const isCoinbase = input.prevout && input.prevout.hash === '0000000000000000000000000000000000000000000000000000000000000000';
if (isCoinbase) {
return `
<div class="tx-io-item"> <div class="tx-io-item">
<div class="tx-io-header"> <div class="tx-io-header">
<span class="tx-io-index">#${i}</span> <span class="tx-io-index">#${i}</span>
<span class="tx-io-value">${formatValue(input.coin.value)}</span> <span class="tx-io-value" style="color: #f7931e;">COINBASE</span>
</div> </div>
<div class="tx-io-address">${input.coin.address}</div>
${input.coin.covenant.action !== 'NONE' ? `<div class="tx-covenant">Covenant: ${input.coin.covenant.action}</div>` : ''}
</div> </div>
`).join('')} `;
} else {
return `
<div class="tx-io-item">
<div class="tx-io-header">
<span class="tx-io-index">#${i}</span>
<span class="tx-io-value">${input.coin ? formatValue(input.coin.value) : 'Unknown'}</span>
</div>
<div class="tx-io-address">${input.coin ? input.coin.address : (input.address || 'Unknown')}</div>
${input.coin && input.coin.covenant.action !== 'NONE' ? `<div class="tx-covenant" data-covenant-action="${input.coin.covenant.action}" data-covenant="${encodeURIComponent(JSON.stringify(input.coin.covenant))}">Covenant: ${input.coin.covenant.action}</div>` : ''}
</div>
`;
}
}).join('')}
</div> </div>
</div> </div>
@@ -395,7 +881,7 @@
<span class="tx-io-value">${formatValue(output.value)}</span> <span class="tx-io-value">${formatValue(output.value)}</span>
</div> </div>
<div class="tx-io-address">${output.address}</div> <div class="tx-io-address">${output.address}</div>
${output.covenant.action !== 'NONE' ? `<div class="tx-covenant">Covenant: ${output.covenant.action}</div>` : ''} ${output.covenant.action !== 'NONE' ? `<div class="tx-covenant" data-covenant-action="${output.covenant.action}" data-covenant="${encodeURIComponent(JSON.stringify(output.covenant))}">Covenant: ${output.covenant.action}</div>` : ''}
</div> </div>
`).join('')} `).join('')}
</div> </div>
@@ -435,6 +921,7 @@
</div> </div>
`; `;
document.body.appendChild(modal); document.body.appendChild(modal);
if (!data.error) updateCovenants();
} }
// Display helper // Display helper
@@ -456,11 +943,88 @@
} }
} }
// Update covenant information from API
async function updateCovenants() {
const elements = document.querySelectorAll('[data-covenant-action]');
const covenantsToFetch = [];
const elementMap = new Map(); // Map JSON string -> Array of elements
for (const el of elements) {
const action = el.dataset.covenantAction;
if (action === 'NONE') continue;
// Skip if already updated
if (el.dataset.covenantUpdated) continue;
// Get full covenant data
let covenantData = null;
if (el.dataset.covenant) {
try {
covenantData = JSON.parse(decodeURIComponent(el.dataset.covenant));
} catch (e) {
console.error('Failed to parse covenant data:', e);
}
}
if (!covenantData) continue;
const key = JSON.stringify(covenantData);
if (!elementMap.has(key)) {
elementMap.set(key, []);
covenantsToFetch.push(covenantData);
}
elementMap.get(key).push(el);
}
if (covenantsToFetch.length === 0) return;
try {
const res = await fetch(`/api/v1/covenant`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(covenantsToFetch)
});
if (res.ok) {
const results = await res.json();
for (const result of results) {
const key = JSON.stringify(result.covenant);
const els = elementMap.get(key);
if (els) {
for (const el of els) {
if (el.classList.contains('tx-covenant')) {
el.innerHTML = `Covenant: ${result.display}`;
} else {
el.innerHTML = result.display;
}
el.dataset.covenantUpdated = "true";
}
}
}
}
} catch (e) {
console.error('Failed to fetch covenant info:', e);
}
}
// Show loading animation
function showLoading(elementId) {
const element = document.getElementById(elementId);
element.style.display = 'block';
element.innerHTML = `
<div class="loading-container">
<div class="loading-spinner"></div>
<div class="loading-text">Lighing Fire...</div>
</div>
`;
}
// Load status on page load // Load status on page load
async function loadStatus() { async function loadStatus() {
const nodeStatus = await apiCall('status');
displayResult('node-status', nodeStatus, 'status');
const chainStatus = await apiCall('chain'); const chainStatus = await apiCall('chain');
if (chainStatus.chain) { if (chainStatus.chain) {
document.getElementById('chain-status').innerHTML = formatChainData(chainStatus.chain); document.getElementById('chain-status').innerHTML = formatChainData(chainStatus.chain);
@@ -483,6 +1047,8 @@
alert('Please enter a block height or hash'); alert('Please enter a block height or hash');
return; return;
} }
showLoading('block-result');
updateURL('block', blockId); updateURL('block', blockId);
const data = await apiCall(`block/${blockId}`); const data = await apiCall(`block/${blockId}`);
@@ -501,6 +1067,8 @@
alert('Please enter a block height or hash'); alert('Please enter a block height or hash');
return; return;
} }
showLoading('block-result');
updateURL('header', blockId); updateURL('header', blockId);
const data = await apiCall(`header/${blockId}`); const data = await apiCall(`header/${blockId}`);
@@ -519,37 +1087,99 @@
alert('Please enter a transaction ID'); alert('Please enter a transaction ID');
return; return;
} }
const resultElement = document.getElementById('tx-result');
showLoading('tx-result');
// Scroll to the search section
document.querySelector('.search-section').scrollIntoView({ behavior: 'smooth', block: 'start' });
updateURL('tx', txId); updateURL('tx', txId);
const data = await apiCall(`tx/${txId}`); const data = await apiCall(`tx/${txId}`);
// Use formatted display instead of raw JSON // Use formatted display instead of raw JSON
const resultElement = document.getElementById('tx-result');
if (data.error) { if (data.error) {
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`; resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else { } else {
resultElement.innerHTML = formatTransactionData(data); resultElement.innerHTML = formatTransactionData(data);
updateCovenants();
} }
} }
async function searchAddressTx() { async function searchAddressTx() {
const address = document.getElementById('address-input').value.trim().replace(/,/g, ''); let address = document.getElementById('address-input').value.trim().replace(/,/g, '');
if (!address) { if (!address) {
alert('Please enter an address'); alert('Please enter an address');
return; return;
} }
showLoading('address-result');
const resultElement = document.getElementById('address-result');
// Check for HIP02 alias
if (address.startsWith('@')) {
const hip02Result = await resolveHip02(address);
if (hip02Result.success && hip02Result.address) {
address = hip02Result.address;
// Update input to show resolved address
document.getElementById('address-input').value = address;
// Add a note about resolution
const note = document.createElement('div');
note.className = 'success-message';
note.style.marginBottom = '1rem';
note.innerHTML = `Resolved <strong>${hip02Result.name || address}</strong> to address <br><span class="mono">${address}</span>`;
resultElement.parentNode.insertBefore(note, resultElement);
setTimeout(() => note.remove(), 5000);
} else {
resultElement.innerHTML = `<div class="error">HIP02 Error: ${hip02Result.error || 'Could not resolve alias'}</div>`;
return;
}
}
updateURL('address', address); updateURL('address', address);
const data = await apiCall(`tx/address/${address}`); const data = await apiCall(`tx/address/${address}`);
displayResult('address-result', data);
// Use formatted display instead of raw JSON
if (data.error) {
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else {
resultElement.innerHTML = formatAddressTransactions(data);
}
} }
async function searchAddressCoins() { async function searchAddressCoins() {
const address = document.getElementById('address-input').value.trim().replace(/,/g, ''); let address = document.getElementById('address-input').value.trim().replace(/,/g, '');
if (!address) { if (!address) {
alert('Please enter an address'); alert('Please enter an address');
return; return;
} }
showLoading('address-result');
const resultElement = document.getElementById('address-result');
// Check for HIP02 alias
if (address.startsWith('@')) {
const hip02Result = await resolveHip02(address);
if (hip02Result.success && hip02Result.address) {
address = hip02Result.address;
// Update input to show resolved address
document.getElementById('address-input').value = address;
} else {
resultElement.innerHTML = `<div class="error">HIP02 Error: ${hip02Result.error || 'Could not resolve alias'}</div>`;
return;
}
}
updateURL('address', address);
const data = await apiCall(`coin/address/${address}`); const data = await apiCall(`coin/address/${address}`);
displayResult('address-result', data);
// Use formatted display instead of raw JSON
if (data.error) {
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else {
resultElement.innerHTML = formatAddressCoins(data);
updateCovenants();
}
} }
async function searchName() { async function searchName() {
@@ -558,9 +1188,19 @@
alert('Please enter a name'); alert('Please enter a name');
return; return;
} }
updateURL('name', name);
const data = await apiCall(`name/${name}`); showLoading('name-result');
displayResult('name-result', data); const punyName = toPunycode(name);
updateURL('name', punyName);
const data = await apiCall(`name/${punyName}`);
// Use formatted display
const resultElement = document.getElementById('name-result');
if (data.error) {
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else {
resultElement.innerHTML = formatNameInfo(data);
}
} }
async function searchNameResource() { async function searchNameResource() {
@@ -569,8 +1209,18 @@
alert('Please enter a name'); alert('Please enter a name');
return; return;
} }
const data = await apiCall(`nameresource/${name}`);
displayResult('name-result', data); showLoading('name-result');
const punyName = toPunycode(name);
const data = await apiCall(`nameresource/${punyName}`);
// Use formatted display
const resultElement = document.getElementById('name-result');
if (data.error) {
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else {
resultElement.innerHTML = formatNameResource(data);
}
} }
async function searchNameSummary() { async function searchNameSummary() {
@@ -579,8 +1229,18 @@
alert('Please enter a name'); alert('Please enter a name');
return; return;
} }
const data = await apiCall(`namesummary/${name}`);
displayResult('name-result', data); showLoading('name-result');
const punyName = toPunycode(name);
const data = await apiCall(`namesummary/${punyName}`);
// Use formatted display
const resultElement = document.getElementById('name-result');
if (data.error) {
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else {
resultElement.innerHTML = formatNameSummary(data);
}
} }
async function searchNameHash() { async function searchNameHash() {
@@ -589,19 +1249,26 @@
alert('Please enter a name hash'); alert('Please enter a name hash');
return; return;
} }
const data = await apiCall(`namehash/${nameHash}`);
displayResult('name-result', data);
}
async function searchCoin() { showLoading('name-result');
const coinHash = document.getElementById('coin-hash').value.trim().replace(/,/g, '');
const coinIndex = document.getElementById('coin-index').value.trim().replace(/,/g, ''); try {
if (!coinHash || !coinIndex) { const response = await fetch(`/api/v1/namehash/${nameHash}`);
alert('Please enter both coin hash and index'); const data = await response.json();
return;
const resultElement = document.getElementById('name-result');
if (data.error) {
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else if (data.name) {
// Valid name found, redirect to name page
window.location.href = `/name/${data.name}`;
} else {
resultElement.innerHTML = `<div class="error">No name found for this hash</div>`;
}
} catch (e) {
const resultElement = document.getElementById('name-result');
resultElement.innerHTML = `<div class="error">Error: ${e.message}</div>`;
} }
const data = await apiCall(`coin/${coinHash}/${coinIndex}`);
displayResult('coin-result', data);
} }
// Load status when page loads // Load status when page loads

225
tools.py Normal file
View File

@@ -0,0 +1,225 @@
import dns.resolver
from cryptography import x509
from cryptography.hazmat.backends import default_backend
import tempfile
import subprocess
import binascii
import datetime
import dns.asyncresolver
import dns.message
import dns.query
import dns.rdatatype
import httpx
from requests_doh import DNSOverHTTPSSession, add_dns_provider
import urllib3
from cryptography.x509.oid import ExtensionOID
urllib3.disable_warnings(
urllib3.exceptions.InsecureRequestWarning
) # Disable insecure request warnings (since we are manually verifying the certificate)
def hip2(domain: str) -> str | None:
domain_check = False
try:
# Get the IP
ip = resolve_with_doh(domain)
# Run the openssl s_client command
s_client_command = [
"openssl",
"s_client",
"-showcerts",
"-connect",
f"{ip}:443",
"-servername",
domain,
]
s_client_process = subprocess.Popen(
s_client_command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE,
)
s_client_output, _ = s_client_process.communicate(input=b"\n")
certificates = []
current_cert = ""
for line in s_client_output.split(b"\n"):
current_cert += line.decode("utf-8") + "\n"
if "-----END CERTIFICATE-----" in line.decode("utf-8"):
certificates.append(current_cert)
current_cert = ""
# Remove anything before -----BEGIN CERTIFICATE-----
certificates = [
cert[cert.find("-----BEGIN CERTIFICATE-----") :] for cert in certificates
]
if certificates:
cert = certificates[0]
with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp_cert_file:
temp_cert_file.write(cert)
temp_cert_file.seek(
0
) # Move back to the beginning of the temporary file
tlsa_command = [
"openssl",
"x509",
"-in",
temp_cert_file.name,
"-pubkey",
"-noout",
"|",
"openssl",
"pkey",
"-pubin",
"-outform",
"der",
"|",
"openssl",
"dgst",
"-sha256",
"-binary",
]
tlsa_process = subprocess.Popen(
" ".join(tlsa_command), shell=True, stdout=subprocess.PIPE
)
tlsa_output, _ = tlsa_process.communicate()
tlsa_server = "3 1 1 " + binascii.hexlify(tlsa_output).decode("utf-8")
# Get domains
cert_obj = x509.load_pem_x509_certificate(
cert.encode("utf-8"), default_backend()
)
domains = []
for ext in cert_obj.extensions:
if ext.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME:
san_list = ext.value.get_values_for_type(x509.DNSName)
domains.extend(san_list)
# Extract the common name (CN) from the subject
common_name = cert_obj.subject.get_attributes_for_oid(
x509.NameOID.COMMON_NAME
)
if common_name:
if common_name[0].value not in domains:
domains.append(common_name[0].value)
if domains:
if domain in domains:
domain_check = True
else:
# Check if matching wildcard domain exists
for d in domains:
if d.startswith("*"):
if domain.split(".")[1:] == d.split(".")[1:]:
domain_check = True
break
expiry_date = cert_obj.not_valid_after_utc
# Check if expiry date is past
if expiry_date < datetime.datetime.now(datetime.timezone.utc):
return
else:
return
try:
# Check for TLSA record
tlsa = resolve_TLSA_with_doh(domain)
if not tlsa:
return
else:
if tlsa_server == str(tlsa):
if domain_check:
# Get the Hip2 addresss from /.well-known/wallets/HNS
add_dns_provider("HNSDoH", "https://au.hnsdoh.com/dns-query")
session = DNSOverHTTPSSession("HNSDoH")
r = session.get(
f"https://{domain}/.well-known/wallets/HNS", verify=False
)
return r.text
else:
return
else:
return
except Exception as e:
print(f"Hip2: TLSA lookup/verification failed with error: {e}", flush=True)
return
# Catch all exceptions
except Exception as e:
print(f"Hip2: Lookup failed with error: {e}", flush=True)
return
def wallet_txt(domain: str, doh_url="https://au.hnsdoh.com/dns-query") -> str | None:
with httpx.Client() as client:
q = dns.message.make_query(domain, dns.rdatatype.from_text("TYPE262"))
r = dns.query.https(q, doh_url, session=client)
if not r.answer:
return
wallet_record = None
for ans in r.answer:
raw = ans[0].to_wire() # type: ignore
try:
data = raw[1:].decode("utf-8", errors="ignore")
except UnicodeDecodeError:
return f"Unknown WALLET record format: {raw.hex()}"
if data.startswith("HNS:"):
wallet_record = data[4:]
break
elif data.startswith("HNS "):
wallet_record = data[4:]
break
elif data.startswith('"HNS" '):
wallet_record = data[6:].strip('"')
break
return wallet_record
def resolve_with_doh(query_name, doh_url="https://au.hnsdoh.com/dns-query"):
with httpx.Client() as client:
q = dns.message.make_query(query_name, dns.rdatatype.A)
r = dns.query.https(q, doh_url, session=client)
ip = r.answer[0][0].address # type: ignore
return ip
def resolve_TLSA_with_doh(query_name, doh_url="https://au.hnsdoh.com/dns-query"):
query_name = "_443._tcp." + query_name
with httpx.Client() as client:
q = dns.message.make_query(query_name, dns.rdatatype.TLSA)
r = dns.query.https(q, doh_url, session=client)
tlsa = r.answer[0][0]
return tlsa
def emoji_to_punycode(emoji):
try:
return emoji.encode("idna").decode("ascii")
except Exception:
return emoji
def punycode_to_emoji(punycode):
try:
return punycode.encode("ascii").decode("idna")
except Exception:
return punycode

373
uv.lock generated
View File

@@ -2,6 +2,19 @@ version = 1
revision = 3 revision = 3
requires-python = ">=3.13" requires-python = ">=3.13"
[[package]]
name = "anyio"
version = "4.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
]
[[package]] [[package]]
name = "blinker" name = "blinker"
version = "1.9.0" version = "1.9.0"
@@ -20,6 +33,51 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
] ]
[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[package]] [[package]]
name = "cfgv" name = "cfgv"
version = "3.5.0" version = "3.5.0"
@@ -91,6 +149,62 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
] ]
[[package]]
name = "cryptography"
version = "46.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" },
{ url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
{ url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
{ url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
{ url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
{ url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
{ url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
{ url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
{ url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
{ url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
{ url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
{ url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
{ url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" },
{ url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" },
{ url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" },
{ url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" },
{ url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" },
{ url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" },
{ url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" },
{ url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" },
{ url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" },
{ url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" },
{ url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" },
{ url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" },
{ url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" },
{ url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" },
{ url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" },
{ url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" },
{ url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" },
{ url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" },
{ url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
{ url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
{ url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
{ url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
{ url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
{ url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
{ url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
{ url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
{ url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
{ url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
{ url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
{ url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
{ url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
{ url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
]
[[package]] [[package]]
name = "distlib" name = "distlib"
version = "0.4.0" version = "0.4.0"
@@ -100,6 +214,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
] ]
[[package]]
name = "dnspython"
version = "2.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/37/7d/c871f55054e403fdfd6b8f65fd6d1c4e147ed100d3e9f9ba1fe695403939/dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc", size = 332727, upload-time = "2024-02-18T18:48:48.952Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/a1/8c5287991ddb8d3e4662f71356d9656d91ab3a36618c3dd11b280df0d255/dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50", size = 307696, upload-time = "2024-02-18T18:48:46.786Z" },
]
[package.optional-dependencies]
doh = [
{ name = "h2" },
{ name = "httpcore" },
{ name = "httpx" },
]
[[package]] [[package]]
name = "filelock" name = "filelock"
version = "3.20.0" version = "3.20.0"
@@ -109,6 +239,41 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
] ]
[[package]]
name = "fireexplorer"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "cryptography" },
{ name = "flask" },
{ name = "gunicorn" },
{ name = "pillow" },
{ name = "python-dotenv" },
{ name = "requests-doh" },
]
[package.dev-dependencies]
dev = [
{ name = "pre-commit" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "cryptography", specifier = ">=46.0.3" },
{ name = "flask", specifier = ">=3.1.2" },
{ name = "gunicorn", specifier = ">=23.0.0" },
{ name = "pillow", specifier = ">=11.3.0" },
{ name = "python-dotenv", specifier = ">=1.2.1" },
{ name = "requests-doh", specifier = ">=1.0.0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "pre-commit", specifier = ">=4.4.0" },
{ name = "ruff", specifier = ">=0.14.5" },
]
[[package]] [[package]]
name = "flask" name = "flask"
version = "3.1.2" version = "3.1.2"
@@ -138,6 +303,74 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
] ]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "h2"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "hpack" },
{ name = "hyperframe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" },
]
[[package]]
name = "hpack"
version = "4.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "hyperframe"
version = "6.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" },
]
[[package]] [[package]]
name = "identify" name = "identify"
version = "2.6.15" version = "2.6.15"
@@ -247,6 +480,64 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
] ]
[[package]]
name = "pillow"
version = "12.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" },
{ url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" },
{ url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" },
{ url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" },
{ url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" },
{ url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" },
{ url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" },
{ url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" },
{ url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" },
{ url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" },
{ url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" },
{ url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" },
{ url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" },
{ url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" },
{ url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" },
{ url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" },
{ url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" },
{ url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" },
{ url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" },
{ url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" },
{ url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" },
{ url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" },
{ url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" },
{ url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" },
{ url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" },
{ url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" },
{ url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" },
{ url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" },
{ url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" },
{ url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" },
{ url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" },
{ url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" },
{ url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" },
{ url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" },
{ url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" },
{ url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" },
{ url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" },
{ url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" },
{ url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" },
{ url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" },
{ url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" },
{ url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" },
{ url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" },
{ url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" },
{ url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" },
{ url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" },
{ url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" },
{ url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" },
]
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.5.0" version = "4.5.0"
@@ -272,6 +563,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/27/11/574fe7d13acf30bfd0a8dd7fa1647040f2b8064f13f43e8c963b1e65093b/pre_commit-4.4.0-py2.py3-none-any.whl", hash = "sha256:b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813", size = 226049, upload-time = "2025-11-08T21:12:10.228Z" }, { url = "https://files.pythonhosted.org/packages/27/11/574fe7d13acf30bfd0a8dd7fa1647040f2b8064f13f43e8c963b1e65093b/pre_commit-4.4.0-py2.py3-none-any.whl", hash = "sha256:b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813", size = 226049, upload-time = "2025-11-08T21:12:10.228Z" },
] ]
[[package]]
name = "pycparser"
version = "2.23"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
]
[[package]]
name = "pysocks"
version = "1.7.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" },
]
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.2.1" version = "1.2.1"
@@ -281,37 +590,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
] ]
[[package]]
name = "python-webserver-template"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "flask" },
{ name = "gunicorn" },
{ name = "python-dotenv" },
{ name = "requests" },
]
[package.dev-dependencies]
dev = [
{ name = "pre-commit" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "flask", specifier = ">=3.1.2" },
{ name = "gunicorn", specifier = ">=23.0.0" },
{ name = "python-dotenv", specifier = ">=1.2.1" },
{ name = "requests", specifier = ">=2.32.5" },
]
[package.metadata.requires-dev]
dev = [
{ name = "pre-commit", specifier = ">=4.4.0" },
{ name = "ruff", specifier = ">=0.14.5" },
]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.3" version = "6.0.3"
@@ -350,7 +628,7 @@ wheels = [
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.5" version = "2.32.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
@@ -358,9 +636,27 @@ dependencies = [
{ name = "idna" }, { name = "idna" },
{ name = "urllib3" }, { name = "urllib3" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
]
[package.optional-dependencies]
socks = [
{ name = "pysocks" },
]
[[package]]
name = "requests-doh"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dnspython", extra = ["doh"] },
{ name = "requests", extra = ["socks"] },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/8d/d9b24a0c0975c9330bcc152af2b5c9a34fa5af0307c10366fdc27e75f24e/requests_doh-1.0.0.tar.gz", hash = "sha256:6ce8bc96245030a198ef20d2100b4dcb3b120a05a58df703f8be121a79f8f2fb", size = 13126, upload-time = "2024-09-07T13:42:55.394Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/f1/79f00f86e53510b75a14dc500286f351550cc8207c81e7de1f38072fbcac/requests_doh-1.0.0-py3-none-any.whl", hash = "sha256:eea6583b792b7d3dfde74fd28eedc2b95d6ea896368119eede31f0d6ff2c838c", size = 13879, upload-time = "2024-09-07T13:42:54.189Z" },
] ]
[[package]] [[package]]
@@ -389,6 +685,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/80/69756670caedcf3b9be597a6e12276a6cf6197076eb62aad0c608f8efce0/ruff-0.14.5-py3-none-win_arm64.whl", hash = "sha256:4b700459d4649e2594b31f20a9de33bc7c19976d4746d8d0798ad959621d64a4", size = 13433331, upload-time = "2025-11-13T19:58:48.434Z" }, { url = "https://files.pythonhosted.org/packages/e5/80/69756670caedcf3b9be597a6e12276a6cf6197076eb62aad0c608f8efce0/ruff-0.14.5-py3-none-win_arm64.whl", hash = "sha256:4b700459d4649e2594b31f20a9de33bc7c19976d4746d8d0798ad959621d64a4", size = 13433331, upload-time = "2025-11-13T19:58:48.434Z" },
] ]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.5.0" version = "2.5.0"