Skip to content

feat: v2 implementation#255

Draft
fsamier wants to merge 13 commits intomainfrom
feat/model-v2
Draft

feat: v2 implementation#255
fsamier wants to merge 13 commits intomainfrom
feat/model-v2

Conversation

@fsamier
Copy link
Contributor

@fsamier fsamier commented Feb 12, 2026

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an ERC-7730 v2 pipeline to the codebase: new v2 input/resolved Pydantic models, an input→resolved converter, v2-specific linters, and golden-file tests validating conversion output.

Changes:

  • Introduce v2 input + resolved descriptor models (context/metadata/display) and discriminated unions.
  • Implement v2 input→resolved conversion (selectors, references, params, constants/maps/enums).
  • Add v2 linting entrypoints + linters and golden reference tests with sample v2 descriptors.

Reviewed changes

Copilot reviewed 66 out of 69 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
tests/v2/convert/resolved/test_convert_input_to_resolved.py Golden-file conversion tests for v2 input→resolved.
tests/v2/convert/resolved/data/calldata_with_extended_params_input.json Input fixture for calldata params conversion.
tests/v2/convert/resolved/data/calldata_with_extended_params_resolved.json Expected resolved output for calldata params conversion.
tests/v2/convert/resolved/data/deprecated_abi_ignored_input.json Input fixture asserting deprecated ABI is ignored.
tests/v2/convert/resolved/data/deprecated_abi_ignored_resolved.json Expected resolved output with deprecated ABI removed/ignored.
tests/v2/convert/resolved/data/deprecated_schemas_ignored_input.json Input fixture asserting deprecated EIP-712 schemas are ignored.
tests/v2/convert/resolved/data/deprecated_schemas_ignored_resolved.json Expected resolved output with deprecated schemas removed/ignored.
tests/v2/convert/resolved/data/field_group_basic_input.json Input fixture for field-group scoping.
tests/v2/convert/resolved/data/field_group_basic_resolved.json Expected resolved output for field-group scoping.
tests/v2/convert/resolved/data/field_group_with_iteration_input.json Input fixture for field-group iteration.
tests/v2/convert/resolved/data/field_group_with_iteration_resolved.json Expected resolved output for field-group iteration.
tests/v2/convert/resolved/data/field_group_with_label_input.json Input fixture for field-group labels.
tests/v2/convert/resolved/data/field_group_with_label_resolved.json Expected resolved output for field-group labels.
tests/v2/convert/resolved/data/field_with_encryption_input.json Input fixture for encryption metadata on fields.
tests/v2/convert/resolved/data/field_with_encryption_resolved.json Expected resolved output for encryption metadata.
tests/v2/convert/resolved/data/field_with_separator_input.json Input fixture for array separator handling.
tests/v2/convert/resolved/data/field_with_separator_resolved.json Expected resolved output for array separator handling.
tests/v2/convert/resolved/data/field_with_visibility_conditions_input.json Input fixture for conditional visibility rules.
tests/v2/convert/resolved/data/field_with_visibility_conditions_resolved.json Expected resolved output for conditional visibility rules.
tests/v2/convert/resolved/data/field_with_visibility_simple_input.json Input fixture for simple visibility rules.
tests/v2/convert/resolved/data/field_with_visibility_simple_resolved.json Expected resolved output for simple visibility rules.
tests/v2/convert/resolved/data/format_chain_id_input.json Input fixture for chainId field format.
tests/v2/convert/resolved/data/format_chain_id_resolved.json Expected resolved output for chainId field format.
tests/v2/convert/resolved/data/format_interoperable_address_name_input.json Input fixture for interoperable address name format.
tests/v2/convert/resolved/data/format_interoperable_address_name_resolved.json Expected resolved output for interoperable address name format.
tests/v2/convert/resolved/data/format_token_ticker_input.json Input fixture for token ticker format.
tests/v2/convert/resolved/data/format_token_ticker_resolved.json Expected resolved output for token ticker format.
tests/v2/convert/resolved/data/format_with_interpolated_intent_input.json Input fixture for interpolated intent strings.
tests/v2/convert/resolved/data/format_with_interpolated_intent_resolved.json Expected resolved output for interpolated intent strings.
tests/v2/convert/resolved/data/metadata_with_contract_name_input.json Input fixture for metadata.contractName.
tests/v2/convert/resolved/data/metadata_with_contract_name_resolved.json Expected resolved output for metadata.contractName.
tests/v2/convert/resolved/data/metadata_with_maps_input.json Input fixture for metadata.maps.
tests/v2/convert/resolved/data/metadata_with_maps_resolved.json Expected resolved output for metadata.maps.
tests/v2/convert/resolved/data/minimal_contract_v2_input.json Minimal contract-context v2 input fixture.
tests/v2/convert/resolved/data/minimal_contract_v2_resolved.json Expected resolved output for minimal contract-context fixture.
tests/v2/convert/resolved/data/minimal_eip712_v2_input.json Minimal EIP-712-context v2 input fixture.
tests/v2/convert/resolved/data/minimal_eip712_v2_resolved.json Expected resolved output for minimal EIP-712-context fixture.
tests/v2/convert/resolved/data/token_amount_with_chainid_input.json Input fixture for tokenAmount + chainIdPath parameters.
tests/v2/convert/resolved/data/token_amount_with_chainid_resolved.json Expected resolved output for tokenAmount + chainIdPath parameters.
tests/v2/convert/resolved/init.py Test package init for v2 resolved conversion tests.
tests/v2/convert/init.py Test package init for v2 conversion tests.
tests/v2/init.py Test package init for v2 tests.
src/erc7730/model/input/v2/init.py v2 input model package init.
src/erc7730/model/input/v2/context.py v2 input context models (contract/EIP-712).
src/erc7730/model/input/v2/descriptor.py v2 input descriptor root model.
src/erc7730/model/input/v2/display.py v2 input display/field/params models (incl. groups, visibility, maps).
src/erc7730/model/input/v2/format.py v2 field format enums.
src/erc7730/model/input/v2/metadata.py v2 input metadata models (incl. maps/enums/constants).
src/erc7730/model/input/v2/unions.py v2 discriminators for fields/params/visibility unions.
src/erc7730/model/resolved/v2/init.py v2 resolved model package init.
src/erc7730/model/resolved/v2/context.py v2 resolved context models with normalized addresses and deprecated fields dropped.
src/erc7730/model/resolved/v2/descriptor.py v2 resolved descriptor root model.
src/erc7730/model/resolved/v2/display.py v2 resolved display/field/params models.
src/erc7730/model/resolved/v2/metadata.py v2 resolved metadata models (resolved enums/maps).
src/erc7730/convert/resolved/v2/init.py v2 conversion helpers package init.
src/erc7730/convert/resolved/v2/constants.py v2 constant provider + path resolution helpers (incl. map reference scaffolding).
src/erc7730/convert/resolved/v2/convert_erc7730_input_to_resolved.py Main v2 input→resolved converter implementation.
src/erc7730/convert/resolved/v2/enums.py Enum reference parsing/validation helpers for v2 conversion.
src/erc7730/convert/resolved/v2/parameters.py v2 parameter resolution for field formats (token amount/ticker, calldata, etc.).
src/erc7730/convert/resolved/v2/references.py v2 display definition reference inlining.
src/erc7730/convert/resolved/v2/values.py v2 field value/path resolution + constant encoding.
src/erc7730/lint/v2/init.py v2 linter base + MultiLinter runner.
src/erc7730/lint/v2/lint.py v2 lint CLI/entrypoint functions (load→convert→lint).
src/erc7730/lint/v2/lint_transaction_type_classifier.py v2 transaction classification linter (permit heuristics + ABI classifier hook).
src/erc7730/lint/v2/lint_validate_display_fields.py v2 ABI-backed display path validation (via explorer ABI fetch).
src/erc7730/lint/v2/lint_validate_max_length.py v2 max-length checks for Ledger device constraints.
src/erc7730/lint/v2/path_schemas.py v2 schema-path extraction for linting.
specs/erc7730-v1.schema.json Adds/records the v1 JSON schema file under specs/.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +257 to +264
token_resolved = resolve_path_or_constant_value(
prefix=prefix,
input_path=params.tokenPath,
input_value=token_value,
abi_type=ABIDataType.ADDRESS,
constants=constants,
out=out,
)
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve_path_or_constant_value(...) can return a ResolvedValuePath, but this branch only handles ResolvedValueConstant. As a result, when tokenPath is provided (the common case), the resolved path is ignored and the returned resolved params won’t carry the correctly prefixed/absolute token path.

Copilot uses AI. Check for mistakes.
Comment on lines +308 to +316
return ResolvedTokenAmountParameters(
tokenPath=params.tokenPath,
token=resolved_token,
nativeCurrencyAddress=resolved_addresses,
threshold=resolved_threshold,
message=constants.resolve_or_none(params.message, out),
chainId=resolved_chain_id,
chainIdPath=params.chainIdPath,
)
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The returned ResolvedTokenAmountParameters passes through tokenPath/chainIdPath from the input without resolving descriptor-path constants or applying the current prefix (unlike field path resolution). This can leave relative paths in the resolved descriptor and break downstream consumers/linting; consider populating these from the resolved path results (absolute/prefixed) and/or resolving via constants.resolve_path + prefix concatenation.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +71
case ResolvedCallDataParameters():
pass
case ResolvedTokenAmountParameters():
if field.params.tokenPath is not None:
add_path(field.params.tokenPath)
case ResolvedTokenTickerParameters():
pass
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

compute_format_schema_paths ignores data paths referenced inside several parameter objects (e.g., ResolvedCallDataParameters.*Path, ResolvedTokenAmountParameters.chainIdPath, ResolvedTokenTickerParameters.chainIdPath). This makes ABI/path validation incomplete and can produce false "missing display field" warnings or miss invalid parameter paths; include these *Path attributes in the collected data_paths.

Copilot uses AI. Check for mistakes.
pass
case _:
assert_never(field.params)
case ResolvedFieldGroup():
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For ResolvedFieldGroup, the group’s own path is not added to the collected schema paths. If the group scopes fields under an array/object path, failing to record it can hide invalid group paths and skew ABI/path coverage calculations; consider calling add_path(field.path) before recursing into field.fields.

Suggested change
case ResolvedFieldGroup():
case ResolvedFieldGroup():
if field.path is not None:
add_path(field.path)

Copilot uses AI. Check for mistakes.
Comment on lines 139 to 149
encoded = value.to_bytes(length=(max(value.bit_length(), 1) + 7) // 8, signed=False)

case ABIDataType.INT:
if not isinstance(value, int):
return out.error(title="Invalid constant", message=f"""Value "{value}" is not an integer""")
encoded = value.to_bytes(length=(max(value.bit_length(), 1) + 7) // 8, signed=True)

case ABIDataType.BOOL:
if not isinstance(value, bool):
return out.error(title="Invalid constant", message=f"""Value "{value}" is not a boolean""")
encoded = value.to_bytes()
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

int.to_bytes(...) is called without the required byteorder argument, which will raise TypeError at runtime for non-hex INT constants. Encode using an explicit byteorder (and keep signed=True for negatives).

Suggested change
encoded = value.to_bytes(length=(max(value.bit_length(), 1) + 7) // 8, signed=False)
case ABIDataType.INT:
if not isinstance(value, int):
return out.error(title="Invalid constant", message=f"""Value "{value}" is not an integer""")
encoded = value.to_bytes(length=(max(value.bit_length(), 1) + 7) // 8, signed=True)
case ABIDataType.BOOL:
if not isinstance(value, bool):
return out.error(title="Invalid constant", message=f"""Value "{value}" is not a boolean""")
encoded = value.to_bytes()
encoded = value.to_bytes(
length=(max(value.bit_length(), 1) + 7) // 8,
byteorder="big",
signed=False,
)
case ABIDataType.INT:
if not isinstance(value, int):
return out.error(title="Invalid constant", message=f"""Value "{value}" is not an integer""")
encoded = value.to_bytes(
length=(max(value.bit_length(), 1) + 7) // 8,
byteorder="big",
signed=True,
)
case ABIDataType.BOOL:
if not isinstance(value, bool):
return out.error(title="Invalid constant", message=f"""Value "{value}" is not a boolean""")
encoded = int(value).to_bytes(length=1, byteorder="big", signed=False)

Copilot uses AI. Check for mistakes.
case ABIDataType.BOOL:
if not isinstance(value, bool):
return out.error(title="Invalid constant", message=f"""Value "{value}" is not a boolean""")
encoded = value.to_bytes()
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value.to_bytes() on a bool will raise TypeError (the method requires length and byteorder). Encode booleans explicitly (e.g., b"\x01"/b"\x00" or int(value).to_bytes(1, byteorder=...)).

Suggested change
encoded = value.to_bytes()
encoded = b"\x01" if value else b"\x00"

Copilot uses AI. Check for mistakes.
from erc7730.common import client
from erc7730.common.abi import reduce_signature, signature_to_selector
from erc7730.common.output import ExceptionsToOutput, OutputAdder
from erc7730.convert import ERC7730Converter
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'ERC7730Converter' is not used.

Copilot uses AI. Check for mistakes.
)

# noinspection PyUnreachableCode
return None
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This statement is unreachable.

Copilot uses AI. Check for mistakes.
fsamier and others added 8 commits February 12, 2026 18:22
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
The lint CLI command was hardcoded to use the v1 model, causing false
errors when linting v2 descriptors. This adds auto-detection based on
the $schema field and routes to the v2 linter accordingly. A --v2 flag
is also available for explicit override.
Pure/view functions cannot produce transactions and don't need clear
signing descriptors. Skip them by default in get_functions() to reduce
noise in lint output. The generator still includes all functions via
include_read_only=True.
rich.print wraps long strings at terminal width, inserting literal
newlines into JSON string values (e.g. hex descriptor fields). This
corrupts the output when piped or captured. Use builtins.print for
schema, resolve, and calldata commands that output raw JSON.
The calldata command now auto-detects v2 descriptors (via $schema field)
and builds ABI Function objects from human-readable format keys in
display.formats, instead of requiring an embedded ABI in the contract
context. Fields with visible="never" are skipped to match v1 behavior.
- `erc7730 resolve` now auto-detects v2 descriptors and uses the v2
  resolver (or accepts --v2 flag).
- `erc7730 convert erc7730-to-eip712` now detects v2 descriptors early
  and exits with a clear error: v2 drops embedded EIP-712 schemas, so
  conversion to legacy format is not supported.
Reconstruct EIP-712 schemas from v2 descriptors where the embedded
schemas have been removed. The display.formats keys (encodeType strings)
are parsed back into full EIP-712 type definitions and the EIP712Domain
is rebuilt from the domain/deployment info.

- Add `parse_encode_type()` utility to reverse encodeType strings
- New `ERC7730V2toEIP712Converter` for v2 -> legacy EIP-712 conversion
- Update `command_convert_erc7730_to_eip712` to dispatch to v2 converter
- Add `validate_eip712_domain_fields()` to warn on non-standard or
  mis-ordered EIP712Domain fields in v1 descriptors
- V2 domain reconstruction emits fields in canonical EIP-712 order:
  name, version, chainId, verifyingContract, salt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants