Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Promotion tools:
- `dapps/README.md` ← `make generate_dapp_list`
- `chains/types/*.json` ← `make generate_type_files`
- `tests/data/xcm_data.json` ← `make generate_test_file`
- `subnets` of `bittensor/v1/config.json` + `icons/bittensor/subnets/*.png` ← `make update-bittensor-subnets` (a daily workflow opens the PR with the run summary in its body; SVG logos need the system cairo library — `brew install cairo` locally). A logo is taken from the first source that works: `bittensor/v1/logo-overrides.json` (hand-picked `{"<netuid>": "https://..."}`, for subnets whose on-chain and CoinGecko logos are wrong or absent), the on-chain subnet identity `logo_url`, then the CoinGecko coin image (from a listing not named like the subnet only CoinGecko's generic τ placeholder is taken, never a previous owner's logo); a failing source keeps the previous logo rather than falling through to a lower one. A dark mark on a transparent background is lightened (hue kept) because the app draws logos on near-black. The generator keeps the other keys of `config.json` as they are: `swapFee`, the Nova fee on subnet swaps as a fraction (`0.003` = 0.3%), is edited by hand. Offline tests: `make test-bittensor`.
- `subnets` of `bittensor/v1/config.json` + `icons/bittensor/subnets/*.png` ← `make update-bittensor-subnets` (a daily workflow opens the PR with the run summary in its body; SVG logos need the system cairo library — `brew install cairo` locally). A logo is taken from the first source that works: `bittensor/v1/logo-overrides.json` (hand-picked `{"<netuid>": "https://..."}`, for subnets whose on-chain and CoinGecko logos are wrong or absent), the on-chain subnet identity `logo_url`, then the CoinGecko coin image (taken as is, so a renamed subnet shows its previous owner's logo until `logo-overrides.json` corrects it); a failing source keeps the previous logo rather than falling through to a lower one. A dark mark on a transparent background is lightened (hue kept) because the app draws logos on near-black. The generator keeps the other keys of `config.json` as they are: `swapFee`, the Nova fee on subnet swaps as a fraction (`0.003` = 0.3%), is edited by hand. Offline tests: `make test-bittensor`.

## Commands

Expand Down
36 changes: 18 additions & 18 deletions bittensor/v1/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"name": "Hone",
"symbol": "ε",
"priceId": "openkaito",
"logo": null
"logo": "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/bittensor/subnets/sn5-eb196de6.png"
},
{
"netuid": 6,
Expand Down Expand Up @@ -118,7 +118,7 @@
"name": "Zero Hour",
"symbol": "π",
"priceId": "bitads",
"logo": null
"logo": "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/bittensor/subnets/sn16-af49c048.png"
},
{
"netuid": 17,
Expand Down Expand Up @@ -174,7 +174,7 @@
"name": "Quasar",
"symbol": "ω",
"priceId": "omega-labs",
"logo": null
"logo": "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/bittensor/subnets/sn24-98e9a6e5.png"
},
{
"netuid": 25,
Expand All @@ -195,7 +195,7 @@
"name": "Orion",
"symbol": "ג",
"priceId": "ni-compute",
"logo": null
"logo": "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/bittensor/subnets/sn27-a447a0cf.png"
},
{
"netuid": 28,
Expand All @@ -209,7 +209,7 @@
"name": "hoτfloaτ",
"symbol": "ה",
"priceId": "coldint",
"logo": null
"logo": "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/bittensor/subnets/sn29-e06270bc.png"
},
{
"netuid": 30,
Expand All @@ -223,7 +223,7 @@
"name": "rec4ll",
"symbol": "ז",
"priceId": "candles",
"logo": null
"logo": "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/bittensor/subnets/sn31-2e7855de.png"
},
{
"netuid": 32,
Expand Down Expand Up @@ -251,7 +251,7 @@
"name": null,
"symbol": "ך",
"priceId": "logicnet",
"logo": null
"logo": "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/bittensor/subnets/sn35-bfb5025c.png"
},
{
"netuid": 36,
Expand Down Expand Up @@ -279,7 +279,7 @@
"name": "deprecated",
"symbol": "מ",
"priceId": "w-ai-parked",
"logo": null
"logo": "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/bittensor/subnets/sn39-47a31583.png"
},
{
"netuid": 40,
Expand All @@ -300,7 +300,7 @@
"name": "Unknown",
"symbol": "ס",
"priceId": "real-time-data-by-masa",
"logo": null
"logo": "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/bittensor/subnets/sn42-eade08f8.png"
},
{
"netuid": 43,
Expand Down Expand Up @@ -405,7 +405,7 @@
"name": null,
"symbol": "ح",
"priceId": "gaia-2",
"logo": null
"logo": "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/bittensor/subnets/sn57-9fe89325.png"
},
{
"netuid": 58,
Expand All @@ -419,7 +419,7 @@
"name": null,
"symbol": "د",
"priceId": "agent-arena-by-masa",
"logo": null
"logo": "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/bittensor/subnets/sn59-877a21b3.png"
},
{
"netuid": 60,
Expand Down Expand Up @@ -496,7 +496,7 @@
"name": "Consted 2.0",
"symbol": "غ",
"priceId": "vericore",
"logo": null
"logo": "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/bittensor/subnets/sn70-64e4c512.png"
},
{
"netuid": 71,
Expand Down Expand Up @@ -594,7 +594,7 @@
"name": null,
"symbol": "ᚨ",
"priceId": "docs-insight-tatsu",
"logo": null
"logo": "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/bittensor/subnets/sn84-63da1bbe.png"
},
{
"netuid": 85,
Expand All @@ -608,7 +608,7 @@
"name": "OnSale",
"symbol": "ᚳ",
"priceId": "miao",
"logo": null
"logo": "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/bittensor/subnets/sn86-1aa23e85.png"
},
{
"netuid": 87,
Expand Down Expand Up @@ -699,7 +699,7 @@
"name": "Thirty Spokes",
"symbol": "გ",
"priceId": "neza",
"logo": null
"logo": "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/bittensor/subnets/sn99-daa6cb7b.png"
},
{
"netuid": 100,
Expand Down Expand Up @@ -727,7 +727,7 @@
"name": "Consted",
"symbol": "Ա",
"priceId": "happyai-2",
"logo": null
"logo": "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/bittensor/subnets/sn103-7a5f20fe.png"
},
{
"netuid": 104,
Expand Down Expand Up @@ -769,7 +769,7 @@
"name": "Finsight",
"symbol": "՞",
"priceId": "taoillium",
"logo": null
"logo": "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/bittensor/subnets/sn109-f0472079.png"
},
{
"netuid": 110,
Expand All @@ -790,7 +790,7 @@
"name": "THOR",
"symbol": "Ђ",
"priceId": "minotaur-2",
"logo": null
"logo": "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/bittensor/subnets/sn112-ad46a972.png"
},
{
"netuid": 113,
Expand Down
Binary file added icons/bittensor/subnets/sn103-7a5f20fe.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/bittensor/subnets/sn109-f0472079.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/bittensor/subnets/sn112-ad46a972.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/bittensor/subnets/sn16-af49c048.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/bittensor/subnets/sn24-98e9a6e5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/bittensor/subnets/sn27-a447a0cf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/bittensor/subnets/sn29-e06270bc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/bittensor/subnets/sn31-2e7855de.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/bittensor/subnets/sn35-bfb5025c.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/bittensor/subnets/sn39-47a31583.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/bittensor/subnets/sn42-eade08f8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/bittensor/subnets/sn5-eb196de6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/bittensor/subnets/sn57-9fe89325.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/bittensor/subnets/sn59-877a21b3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/bittensor/subnets/sn70-64e4c512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/bittensor/subnets/sn84-63da1bbe.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/bittensor/subnets/sn86-1aa23e85.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/bittensor/subnets/sn99-daa6cb7b.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 3 additions & 11 deletions scripts/bittensor/price_source.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import re
from collections import defaultdict
from dataclasses import dataclass
from typing import Dict, List

import requests
Expand All @@ -19,12 +18,6 @@ class PriceSourceError(Exception):
pass


@dataclass(frozen=True)
class CoinGeckoLogo:
name: str
url: str


def _coins_by_netuid(coins: List[dict]) -> Dict[int, dict]:
by_netuid = defaultdict(list)
for coin in coins:
Expand All @@ -42,11 +35,10 @@ def price_ids_from_coins(coins: List[dict]) -> Dict[int, str]:
return price_ids


def logos_from_coins(coins: List[dict]) -> Dict[int, CoinGeckoLogo]:
"""The CoinGecko coin image, a fallback for subnets without a usable on-chain logo. The coin name
comes along so that callers can tell a stale listing of a renamed subnet from a current one."""
def logos_from_coins(coins: List[dict]) -> Dict[int, str]:
"""The CoinGecko coin image URL per netuid, a fallback for subnets without a usable on-chain logo."""
return {
netuid: CoinGeckoLogo(name=coin.get("name") or "", url=coin["image"])
netuid: coin["image"]
for netuid, coin in _coins_by_netuid(coins).items()
if isinstance(coin.get("image"), str) and coin["image"].startswith("https://")
}
Expand Down
62 changes: 8 additions & 54 deletions scripts/bittensor/update_subnets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

A subnet logo comes from the first source that yields an image: a hand-picked URL in
bittensor/v1/logo-overrides.json, the logo_url of the on-chain subnet identity, then the CoinGecko
coin image. A renamed subnet keeps the CoinGecko listing of its previous owner, so when the coin is
not named like the subnet only CoinGecko's generic TAO placeholder is taken from it, never the old
owner's logo. Every other key of config.json, such as swapFee, is maintained by hand and kept as it is.
coin image. The CoinGecko image is taken as is, like Talisman does: a renamed subnet keeps the
listing of its previous owner, so when that logo is wrong put the right one into logo-overrides.json.
Every other key of config.json, such as swapFee, is maintained by hand and kept as it is.

Run from the repo root: `make update-bittensor-subnets`. Pass `--report FILE` to also write the
summary printed at the end to FILE, the daily workflow puts it into the pull request body.
Expand All @@ -14,7 +14,6 @@
import io
import json
import os
import re
import sys
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass, field
Expand All @@ -25,9 +24,7 @@

from scripts.bittensor.chain_source import ChainSourceError, Subnet, bittensor_node_urls, fetch_subnets
from scripts.bittensor.logo_normalizer import LogoError, normalize
from scripts.bittensor.price_source import (
CoinGeckoLogo, PriceSourceError, fetch_coins, logos_from_coins, price_ids_from_coins,
)
from scripts.bittensor.price_source import PriceSourceError, fetch_coins, logos_from_coins, price_ids_from_coins

CONFIG_JSON = "bittensor/v1/config.json"
LOGO_OVERRIDES_JSON = "bittensor/v1/logo-overrides.json"
Expand All @@ -41,15 +38,6 @@
# Per netuid: normalised PNG bytes on success, a human-readable reason on failure.
LogoOutcome = Union[bytes, str]

# CoinGecko gives every subnet token without an own logo the TAO logo, recognised here by its
# perceptual hash (see dhash) so that a rescaled or re-encoded copy still matches.
TAO_LOGO_DHASH = 0x3000707010700010
TAO_LOGO_MAX_DISTANCE = 4
# A name shorter than this says too little to confirm that two listings are the same project.
MIN_NAME_LENGTH = 3

_NOT_ALNUM = re.compile(r"[^0-9a-z]+")


class UpdateAborted(Exception):
pass
Expand All @@ -59,9 +47,6 @@ class UpdateAborted(Exception):
class LogoCandidate:
source: str
url: str
# The listing is not named like the subnet: its own logo may belong to a previous owner, so
# only the generic TAO placeholder, which cannot be wrong, is accepted from it.
placeholder_only: bool = False


LogoCandidates = List[LogoCandidate]
Expand Down Expand Up @@ -157,25 +142,6 @@ def download(url: str) -> bytes:
return b"".join(chunks)


def dhash(png: bytes, size: int = 8) -> int:
"""Difference hash over a tiny grayscale copy, flattened onto white: the same picture at another
size or encoding gives (nearly) the same bits, a different picture gives mostly different ones."""
with Image.open(io.BytesIO(png)) as img:
rgba = img.convert("RGBA")
flat = Image.new("RGBA", rgba.size, (255, 255, 255, 255))
flat.alpha_composite(rgba)
gray = flat.convert("L").resize((size + 1, size), Image.LANCZOS)
bits = 0
for y in range(size):
for x in range(size):
bits = (bits << 1) | (gray.getpixel((x, y)) > gray.getpixel((x + 1, y)))
return bits


def is_tao_placeholder(png: bytes) -> bool:
return bin(dhash(png) ^ TAO_LOGO_DHASH).count("1") <= TAO_LOGO_MAX_DISTANCE


def fetch_logo(candidates: LogoCandidates, keep_previous: bool = False) -> LogoOutcome:
"""The normalised logo of the first candidate that downloads and decodes, else why each one failed.

Expand All @@ -184,27 +150,16 @@ def fetch_logo(candidates: LogoCandidates, keep_previous: bool = False) -> LogoO
failures = []
for candidate in candidates:
try:
png = normalize(download(candidate.url))
if candidate.placeholder_only and not is_tao_placeholder(png):
raise LogoError("listed under another name, only the TAO placeholder is taken from it")
return png
return normalize(download(candidate.url))
except (requests.RequestException, LogoError) as e:
failures.append(f"{candidate.source} {candidate.url}: {type(e).__name__}: {e}")
if keep_previous:
break
return "; ".join(failures)


def names_match(subnet_name: Optional[str], coin_name: Optional[str]) -> bool:
"""Whether a CoinGecko listing still describes the subnet: one normalised name contains the other."""
first, second = (_NOT_ALNUM.sub("", (name or "").lower()) for name in (subnet_name, coin_name))
if min(len(first), len(second)) < MIN_NAME_LENGTH:
return False
return first in second or second in first


def logo_candidates(
subnets: List[Subnet], overrides: Dict[int, str], coingecko: Dict[int, CoinGeckoLogo],
subnets: List[Subnet], overrides: Dict[int, str], coingecko: Dict[int, str],
) -> Dict[int, LogoCandidates]:
candidates = {}
for subnet in subnets:
Expand All @@ -213,9 +168,8 @@ def logo_candidates(
found.append(LogoCandidate("override", overrides[subnet.netuid]))
if subnet.logo_url:
found.append(LogoCandidate("chain", subnet.logo_url))
coin = coingecko.get(subnet.netuid)
if coin:
found.append(LogoCandidate("coingecko", coin.url, placeholder_only=not names_match(subnet.name, coin.name)))
if subnet.netuid in coingecko:
found.append(LogoCandidate("coingecko", coingecko[subnet.netuid]))
if found:
candidates[subnet.netuid] = found
return candidates
Expand Down
Loading
Loading