mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-utils.git
synced 2026-10-20 18:56:26 +00:00
* fix(chains): drop node URLs that embed Nova's Infura keys
Nova's configs carry Nova's own Infura project keys inside node URLs, and
every sync copied them into ours: Ethereum had four keyed Infura nodes and
Base one, across 43 published files. Our Android build already fills
`{INFURA_API_KEY}` from our own secret; the keyed URLs used another
organisation's quota and would break for every user when Nova rotates them.
- sync_from_nova.py strips nodes whose URL embeds an Infura key, and drops a
chain left with no node (only Ethereum - Goerli, a retired testnet, in the
dev files); merge-chains.py applies the same filter
- the published chain files are rewritten with that filter; nothing else in
them changes (each file round-trips byte for byte through json.dump)
- check_pezkuwi_integrity.py refuses a keyed Infura URL under chains/
* fix(chains): strip every keyed provider URL, and make the gate fail closed
The first pass matched only `infura.io/.../v3/<lowercase hex>`, so it missed
`https://...infura.io/v3/<key>`, upper-case keys and hosts, and Nova's
OnFinality `?apikey=` URLs (Moonriver, and the Hades testnet in v20/v21
preConfigured). preConfigured was copied from Nova without the filter, and the
integrity check passed if grep itself failed.
- one predicate, is_keyed(), in sync_from_nova.py: Infura /v3/ ids and
apikey/api_key/key/token query values, case-insensitive; {PLACEHOLDER}
values are allowed
- preConfigured details are filtered after the copy; a chain left with no
node loses its details file and its index entry (Hades, v20/v21 only)
- rewritten files keep their own indent, so the diff is the data change only
- check 5 imports is_keyed() and checks every string in every JSON file
under chains/; an unreadable file or an empty tree is an error
- check 3 reports a grep error instead of passing on it
158 lines
6.1 KiB
Python
Executable File
158 lines
6.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Refuse the five regressions this repo has already had.
|
|
|
|
Each check below exists because the thing it forbids happened, was fixed, and in two
|
|
cases came back. They are cheap; the failures they catch are not.
|
|
|
|
Run from the repository root. Exits non-zero on the first category that fails, after
|
|
reporting every problem it found.
|
|
"""
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
APP_CHAINS = ROOT / "chains" / "v22" / "android" / "chains.json"
|
|
AH = "Pezkuwi Asset Hub"
|
|
|
|
errors: list[str] = []
|
|
|
|
|
|
def load(path: Path):
|
|
with path.open(encoding="utf-8") as fh:
|
|
return json.load(fh)
|
|
|
|
|
|
def asset_hub_assets(doc):
|
|
for chain in doc:
|
|
if chain.get("name") == AH:
|
|
yield from chain.get("assets", [])
|
|
|
|
|
|
def extras(asset):
|
|
return asset.get("typeExtras") or {}
|
|
|
|
|
|
# ── 1. Only assets that exist on chain may be declared ───────────────────────
|
|
#
|
|
# 1001/1002/1003 were declared as DOT/ETH/BTC with icons pointing at Nova's own repo —
|
|
# copied from a template, never created on Pezkuwi Asset Hub. Queried 2026-08-11 against
|
|
# wss://asset-hub-rpc.pezkuwichain.io, assets.asset() returns None for all three; only
|
|
# 1 (PEZ) and 1000 (wUSDT) are Live. Removed twice, back twice.
|
|
GHOST = {"1001", "1002", "1003"}
|
|
for name in ("chains/chains.json",
|
|
"chains/v22/android/chains.json",
|
|
"chains/v22/android/chains_minimal.json"):
|
|
path = ROOT / name
|
|
if not path.exists():
|
|
continue
|
|
bad = [a.get("symbol") for a in asset_hub_assets(load(path))
|
|
if str(extras(a).get("assetId", "")) in GHOST]
|
|
if bad:
|
|
errors.append(
|
|
f"{name}: declares Asset Hub assets that do not exist on chain: {bad}"
|
|
)
|
|
|
|
# ── 2. Sufficient assets must say so ─────────────────────────────────────────
|
|
#
|
|
# Absent, LocalToDomainChainMapper resolves isSufficient to false, the wallet's
|
|
# DeadRecipientValidation runs at ERROR level, and every transfer to an account holding
|
|
# no HEZ is refused — which is every fresh deposit address, so nobody can fund an
|
|
# exchange account. Both assets are sufficient=true on chain.
|
|
if APP_CHAINS.exists():
|
|
declared = {
|
|
str(extras(a).get("assetId"))
|
|
for a in asset_hub_assets(load(APP_CHAINS))
|
|
if extras(a).get("isSufficient") is True
|
|
}
|
|
missing = {"1", "1000"} - declared
|
|
if missing:
|
|
errors.append(
|
|
f"{APP_CHAINS.name}: Asset Hub assets {sorted(missing)} must declare "
|
|
f"isSufficient: true — the chain says they are sufficient"
|
|
)
|
|
|
|
# ── 3. Assets are served from a branch that is actually served ───────────────
|
|
#
|
|
# 252 icon references once pointed at pending/post-fix-release. Every file they named
|
|
# also existed on master, so the dependency bought nothing — and would have broken
|
|
# silently the day that branch was tidied away.
|
|
grep = subprocess.run(
|
|
[
|
|
"grep",
|
|
"-rlE",
|
|
r"pezkuwi-wallet-utils/(pending|feature|fix|test)/",
|
|
str(ROOT / "chains"),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if grep.returncode not in (0, 1):
|
|
errors.append(f"grep for working-branch asset URLs failed: {grep.stderr.strip()}")
|
|
if grep.stdout.strip():
|
|
files = grep.stdout.strip().splitlines()
|
|
errors.append(
|
|
f"{len(files)} file(s) under chains/ reference a working branch for "
|
|
f"assets; use master, e.g. {Path(files[0]).name}"
|
|
)
|
|
|
|
# ── 4. A published fixture matches its source ────────────────────────────────
|
|
#
|
|
# sync_from_nova.py does not copy tests/, so these two are kept in step by hand and
|
|
# nothing noticed if they drifted.
|
|
for fixture in ("pezkuwi_assets_for_testBalance.json",):
|
|
src = ROOT / "pezkuwi-overlay" / "tests" / fixture
|
|
pub = ROOT / "tests" / fixture
|
|
if src.exists() and pub.exists() and src.read_bytes() != pub.read_bytes():
|
|
errors.append(
|
|
f"tests/{fixture} has drifted from pezkuwi-overlay/tests/{fixture}"
|
|
)
|
|
|
|
# ── 5. No third-party API key in a URL ───────────────────────────────────────
|
|
#
|
|
# Nova's configs shipped Nova's own provider keys (Infura project ids, OnFinality
|
|
# apikeys) inside node URLs, and every sync copied them into ours. Our builds fill
|
|
# {INFURA_API_KEY}-style placeholders from our own secrets; a keyed URL is someone
|
|
# else's quota and dies when they rotate it. The predicate is the one the sync
|
|
# uses, and every string in every JSON file is checked, so a file that does not
|
|
# parse is a failure, not a pass.
|
|
sys.path.insert(0, str(ROOT / "scripts"))
|
|
from sync_from_nova import is_keyed # noqa: E402
|
|
|
|
keyed_files = []
|
|
json_files = sorted(p for p in (ROOT / "chains").rglob("*.json"))
|
|
if not json_files:
|
|
errors.append("chains/ has no JSON files; the key check would have checked nothing")
|
|
for path in json_files:
|
|
try:
|
|
doc = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, ValueError) as exc:
|
|
errors.append(f"{path.relative_to(ROOT)} could not be read for the key check: {exc}")
|
|
continue
|
|
stack = [doc]
|
|
while stack:
|
|
node = stack.pop()
|
|
if isinstance(node, dict):
|
|
stack.extend(node.values())
|
|
elif isinstance(node, list):
|
|
stack.extend(node)
|
|
elif isinstance(node, str) and is_keyed(node):
|
|
keyed_files.append(path)
|
|
break
|
|
if keyed_files:
|
|
errors.append(
|
|
f"{len(keyed_files)} file(s) under chains/ embed a provider key in a URL; "
|
|
f"use a {{PLACEHOLDER}}, e.g. {keyed_files[0].relative_to(ROOT)}"
|
|
)
|
|
|
|
if errors:
|
|
for e in errors:
|
|
print(f"::error::{e}")
|
|
print(f"\n{len(errors)} integrity problem(s).", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
print("Pezkuwi integrity: on-chain assets only, sufficiency declared, "
|
|
"assets served from master, fixture in step with its source, "
|
|
"no third-party key in a node URL.")
|