From aa401b253b74efa2dc505e629d7016b258d705bb Mon Sep 17 00:00:00 2001 From: Pi Income Agent Date: Thu, 3 Sep 2026 08:08:19 +0200 Subject: [PATCH] feat(bridges): validate supported route metadata (#2624) --- meta/columns.json | 11 +++++++++ tools/schema.json | 19 +++++++++++++++ tools/validate.py | 60 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+) diff --git a/meta/columns.json b/meta/columns.json index f4073f6ee5..e68ea0379c 100644 --- a/meta/columns.json +++ b/meta/columns.json @@ -450,6 +450,17 @@ "cellType": "arrayPopover", "group": "capabilities" }, + "supportedRoutes": { + "key": "supportedRoutes", + "label": "Supported Routes", + "icon": "lucide:Route", + "description": "Verified ordered source-to-destination bridge routes.", + "filter": "none", + "sorting": "arrayLength", + "pinning": null, + "cellType": "arrayPopover", + "group": "capabilities" + }, "price": { "key": "price", "label": "Price", diff --git a/tools/schema.json b/tools/schema.json index 345bae747d..e54faea9e1 100644 --- a/tools/schema.json +++ b/tools/schema.json @@ -261,6 +261,25 @@ "type": ["array", "null"], "items": { "type": "string" } }, + "supportedRoutes": { + "type": ["array", "null"], + "uniqueItems": true, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["sourceChain", "destinationChain"], + "properties": { + "sourceChain": { "type": "string", "minLength": 1 }, + "destinationChain": { "type": "string", "minLength": 1 }, + "assetSymbols": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "type": "string", "minLength": 1 } + } + } + } + }, "starred": { "type": "boolean" }, "availableApis": { "type": ["array", "null"], diff --git a/tools/validate.py b/tools/validate.py index 76c9da3aa9..74042976de 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -95,6 +95,65 @@ def rule_chain_is_lowercase(data): errors.append(f"Item {idx}: chain must be lowercase: want '{item['chain'].lower()}', got '{item['chain']}'. Please check all categories for the current network.") return errors +def rule_supported_routes_valid(data): + errors = [] + for idx, item in enumerate(data): + routes = item.get("supportedRoutes") + if routes is None or not isinstance(routes, list): + continue + + supported_chains = item.get("supportedChains") + supported_set = set(supported_chains) if isinstance(supported_chains, list) else None + seen = set() + route_order = [] + + for route_idx, route in enumerate(routes): + if not isinstance(route, dict): + continue # JSON Schema reports the shape error. + + source = route.get("sourceChain") + destination = route.get("destinationChain") + if not isinstance(source, str) or not isinstance(destination, str): + continue # JSON Schema reports missing/non-string endpoints. + + location = f"Item {idx}: supportedRoutes[{route_idx}]" + source_normalized = source.strip() + destination_normalized = destination.strip() + if source != source_normalized or destination != destination_normalized: + errors.append(f"{location}: route endpoints must be trimmed") + if not source_normalized or not destination_normalized: + errors.append(f"{location}: route endpoints must not be blank") + if source_normalized == destination_normalized: + errors.append(f"{location}: sourceChain and destinationChain must differ") + + if supported_set is not None: + if source not in supported_set: + errors.append(f"{location}: sourceChain '{source}' is not in supportedChains") + if destination not in supported_set: + errors.append(f"{location}: destinationChain '{destination}' is not in supportedChains") + + assets = route.get("assetSymbols") + if isinstance(assets, list): + assets_are_strings = all(isinstance(asset, str) for asset in assets) + if not assets_are_strings or any(asset != asset.strip() for asset in assets): + errors.append(f"{location}: assetSymbols must contain trimmed strings") + if assets_are_strings and assets != sorted(assets): + errors.append(f"{location}: assetSymbols must be sorted alphabetically") + if assets_are_strings and len(assets) != len(set(assets)): + errors.append(f"{location}: assetSymbols must not contain duplicates") + + fingerprint = json.dumps(route, sort_keys=True, separators=(",", ":")) + if fingerprint in seen: + errors.append(f"{location}: duplicate route object") + seen.add(fingerprint) + route_order.append((source, destination)) + + if route_order != sorted(route_order): + errors.append(f"Item {idx}: supportedRoutes must be sorted by sourceChain, then destinationChain") + + return errors + + def has_unclosed_markdown(s: str) -> bool: if type(s) != str: return False @@ -291,6 +350,7 @@ def main(): rules.add_rule(rule_provider_casing_consistent) rules.add_rule(rule_slug_kebab_case) rules.add_rule(rule_chain_is_lowercase) + rules.add_rule(rule_supported_routes_valid) # Validate networks validator = Draft202012Validator(schema)