From 04b0f323536aa89f926d3ecaf01d439f42bc355a Mon Sep 17 00:00:00 2001 From: venugopalanvip <102638505+venugopalanvip@users.noreply.github.com> Date: Sat, 19 Sep 2026 16:39:53 +0530 Subject: [PATCH] feat(schema): add --v2, so the v2 model's schema can be printed `erc7730 schema` was the only command with no way to reach the v2 models. `lint`, `resolve`, `calldata` and `convert` all take --v2 or detect it from $schema; `schema` printed the v1 model either way, so the schema that `erc7730 lint --v2` actually enforces could not be obtained from the tool. That is the schema worth having. A descriptor is validated twice by the clear-signing registry: against `specs/erc7730-v2.schema.json`, which is maintained by hand, and against these models, by `erc7730 lint`. When the two disagree the descriptor passes one check and fails the other, which is what issue #338 is. Printing the model's schema is how that comparison gets made. The flag switches both forms, so `schema resolved --v2` works too. Tests cover all four combinations and assert the flag reaches the v2 model rather than only that a schema was printed: `mustMatch` is a v2 visibility rule and `excluded` a v1 field, so each name appears in exactly one of the two schemas. Co-Authored-By: Claude Opus 5 --- src/erc7730/main.py | 30 +++++++++++++++++++++++------- tests/test_main.py | 21 +++++++++++++++++++++ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/erc7730/main.py b/src/erc7730/main.py index 13c04979..1eb3050d 100644 --- a/src/erc7730/main.py +++ b/src/erc7730/main.py @@ -75,19 +75,35 @@ def _any_v2_descriptor(paths: list[Path]) -> bool: short_help="Print ERC-7730 descriptor JSON schema.", help=""" Print ERC-7730 descriptor JSON schema. + + The schema is generated from the model this tool validates against, so it is the one + `erc7730 lint` enforces. Pass --v2 for the v2 model. """, ) def command_schema( model_type: Annotated[ERC7730ModelType, Argument(help="The descriptor form ")] = ERC7730ModelType.INPUT, + v2: Annotated[bool, Option("--v2", help="Print the schema of the v2 model")] = False, ) -> None: descriptor_type: type[Model] - match model_type: - case ERC7730ModelType.INPUT: - descriptor_type = InputERC7730Descriptor - case ERC7730ModelType.RESOLVED: - descriptor_type = ResolvedERC7730Descriptor - case _: - assert_never(model_type) + if v2: + from erc7730.model.input.v2.descriptor import InputERC7730Descriptor as InputERC7730DescriptorV2 + from erc7730.model.resolved.v2.descriptor import ResolvedERC7730Descriptor as ResolvedERC7730DescriptorV2 + + match model_type: + case ERC7730ModelType.INPUT: + descriptor_type = InputERC7730DescriptorV2 + case ERC7730ModelType.RESOLVED: + descriptor_type = ResolvedERC7730DescriptorV2 + case _: + assert_never(model_type) + else: + match model_type: + case ERC7730ModelType.INPUT: + descriptor_type = InputERC7730Descriptor + case ERC7730ModelType.RESOLVED: + descriptor_type = ResolvedERC7730Descriptor + case _: + assert_never(model_type) builtins.print(json.dumps(descriptor_type.model_json_schema(by_alias=True), indent=4)) diff --git a/tests/test_main.py b/tests/test_main.py index fac9e546..046823dd 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -34,6 +34,27 @@ def test_schema(model_type: ERC7730ModelType) -> None: assert json.loads(out) is not None +@pytest.mark.parametrize("model_type", list(ERC7730ModelType)) +def test_schema_v2(model_type: ERC7730ModelType) -> None: + result = runner.invoke(app, ["schema", model_type, "--v2"]) + out = "".join(result.stdout.splitlines()) + assert result.exit_code == 0 + assert json.loads(out) is not None + + +def test_schema_v2_reaches_the_v2_model() -> None: + """Exit code 0 only proves a schema was printed, not which model it came from. + + `mustMatch` is a v2 visibility rule and `excluded` a v1 field, so each name appears + in exactly one of the two schemas. + """ + v1 = "".join(runner.invoke(app, ["schema", ERC7730ModelType.INPUT]).stdout.splitlines()) + v2 = "".join(runner.invoke(app, ["schema", ERC7730ModelType.INPUT, "--v2"]).stdout.splitlines()) + + assert "excluded" in v1 and "mustMatch" not in v1 + assert "mustMatch" in v2 and "excluded" not in v2 + + def test_list() -> None: result = runner.invoke(app, ["list", str(ERC7730_REGISTRY_ROOT)]) out = "".join(result.stdout.splitlines())