Skip to content

Commit

Permalink
fix(BACK-8037): EIP-712 conversion must not use format=amount inside …
Browse files Browse the repository at this point in the history
…arrays (#152)

* fix(BACK-8037): EIP-712 conversion must not use format=amount inside arrays

* fix(BACK-8037): fix import
  • Loading branch information
jnicoulaud-ledger authored Nov 21, 2024
1 parent 34cfbfa commit f62f2b6
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 16 deletions.
44 changes: 29 additions & 15 deletions src/erc7730/convert/ledger/eip712/convert_erc7730_to_eip712.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from erc7730.convert import ERC7730Converter
from erc7730.model.context import EIP712Schema
from erc7730.model.display import FieldFormat
from erc7730.model.paths import ContainerField, ContainerPath, DataPath
from erc7730.model.paths import Array, ContainerField, ContainerPath, DataPath
from erc7730.model.paths.path_ops import data_path_concat, to_relative
from erc7730.model.resolved.context import ResolvedDeployment, ResolvedEIP712Context
from erc7730.model.resolved.descriptor import ResolvedERC7730Descriptor
Expand Down Expand Up @@ -132,10 +132,20 @@ def convert_field_description(
field_path: DataPath
asset_path: DataPath | None = None
field_format: EIP712Format | None = None
in_array: bool = False

match field.path:
case DataPath() as field_path:
field_path = data_path_concat(prefix, field_path)

for element in field_path.elements:
match element:
case Array():
in_array = True
break
case _:
pass

case ContainerPath() as container_path:
return out.error(f"Path {container_path} is not supported")
case _:
Expand Down Expand Up @@ -163,20 +173,24 @@ def convert_field_description(
case FieldFormat.AMOUNT:
field_format = EIP712Format.AMOUNT
case FieldFormat.TOKEN_AMOUNT:
field_format = EIP712Format.AMOUNT
if field.params is not None and isinstance(field.params, ResolvedTokenAmountParameters):
match field.params.tokenPath:
case None:
pass
case DataPath() as token_path:
asset_path = data_path_concat(prefix, token_path)
case ContainerPath() as container_path if container_path.field == ContainerField.TO:
# In EIP-712 protocol, format=token with no token path => refers to verifyingContract
asset_path = None
case ContainerPath() as container_path:
return out.error(f"Path {container_path} is not supported")
case _:
assert_never(field.params.tokenPath)
if in_array:
# EIP-712 does not support token references in arrays, fallback to raw format
field_format = EIP712Format.RAW
else:
field_format = EIP712Format.AMOUNT
if field.params is not None and isinstance(field.params, ResolvedTokenAmountParameters):
match field.params.tokenPath:
case None:
pass
case DataPath() as token_path:
asset_path = data_path_concat(prefix, token_path)
case ContainerPath() as container_path if container_path.field == ContainerField.TO:
# In EIP-712 protocol, format=token with no token path => refers to verifyingContract
asset_path = None
case ContainerPath() as container_path:
return out.error(f"Path {container_path} is not supported")
case _:
assert_never(field.params.tokenPath)
case _:
assert_never(field.format)

Expand Down
7 changes: 7 additions & 0 deletions tests/assertions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from pathlib import Path
from typing import Any

import jsonschema
Expand All @@ -19,6 +20,12 @@ def assert_json_str_equals(expected: str, actual: str) -> None:
assert_dict_equals(json.loads(expected), json.loads(actual))


def assert_json_file_equals(expected: Path, actual: Path) -> None:
"""Assert deserialized JSON files are equal."""
with open(expected) as exp, open(actual) as act:
assert_dict_equals(json.load(exp), json.load(act))


def assert_model_json_equals(expected: _BaseModel, actual: _BaseModel) -> None:
"""Assert models serialize to same JSON, pretty printing differences to console."""
assert_json_str_equals(model_to_json_str(expected), model_to_json_str(actual))
Expand Down
174 changes: 174 additions & 0 deletions tests/convert/ledger/eip712/data/eip712-UniswapX-DutchOrder.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
{
"blockchainName": "ethereum",
"chainId": 1,
"name": "Permit2",
"contracts": [
{
"address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
"contractName": "Uniswap",
"messages": [
{
"schema": {
"DutchOrder": [
{
"name": "info",
"type": "OrderInfo"
},
{
"name": "decayStartTime",
"type": "uint256"
},
{
"name": "decayEndTime",
"type": "uint256"
},
{
"name": "inputToken",
"type": "address"
},
{
"name": "inputStartAmount",
"type": "uint256"
},
{
"name": "inputEndAmount",
"type": "uint256"
},
{
"name": "outputs",
"type": "DutchOutput[]"
}
],
"DutchOutput": [
{
"name": "token",
"type": "address"
},
{
"name": "startAmount",
"type": "uint256"
},
{
"name": "endAmount",
"type": "uint256"
},
{
"name": "recipient",
"type": "address"
}
],
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"OrderInfo": [
{
"name": "reactor",
"type": "address"
},
{
"name": "swapper",
"type": "address"
},
{
"name": "nonce",
"type": "uint256"
},
{
"name": "deadline",
"type": "uint256"
},
{
"name": "additionalValidationContract",
"type": "address"
},
{
"name": "additionalValidationData",
"type": "bytes"
}
],
"PermitWitnessTransferFrom": [
{
"name": "permitted",
"type": "TokenPermissions"
},
{
"name": "spender",
"type": "address"
},
{
"name": "nonce",
"type": "uint256"
},
{
"name": "deadline",
"type": "uint256"
},
{
"name": "witness",
"type": "DutchOrder"
}
],
"TokenPermissions": [
{
"name": "token",
"type": "address"
},
{
"name": "amount",
"type": "uint256"
}
]
},
"mapper": {
"label": "UniswapX Dutch Order",
"fields": [
{
"path": "spender",
"label": "Approve to spender",
"format": "raw"
},
{
"path": "permitted.amount",
"label": "Approve amount",
"assetPath": "permitted.token",
"format": "amount"
},
{
"path": "witness.inputStartAmount",
"label": "Spend max",
"assetPath": "witness.inputToken",
"format": "amount"
},
{
"path": "witness.outputs.[].endAmount",
"label": "Minimum amounts to receive",
"format": "raw"
},
{
"path": "witness.outputs.[].recipient",
"label": "On Addresses",
"format": "raw"
},
{
"path": "deadline",
"label": "Approval expire",
"format": "datetime"
}
]
}
}
]
}
]
}
28 changes: 27 additions & 1 deletion tests/convert/ledger/eip712/test_convert_erc7730_to_eip712.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
from pathlib import Path

import pytest
from eip712.model.input.descriptor import InputEIP712DAppDescriptor

from erc7730.common.json import dict_from_json_file
from erc7730.common.pydantic import model_to_json_dict
from erc7730.convert.convert import convert_and_print_errors
from erc7730.convert.ledger.eip712.convert_erc7730_to_eip712 import ERC7730toEIP712Converter
from erc7730.convert.resolved.convert_erc7730_input_to_resolved import ERC7730InputToResolved
from erc7730.model.input.descriptor import InputERC7730Descriptor
from erc7730.model.resolved.descriptor import ResolvedERC7730Descriptor
from tests.assertions import assert_dict_equals
from tests.cases import path_id
from tests.files import ERC7730_EIP712_DESCRIPTORS
from tests.schemas import assert_valid_legacy_eip_712
from tests.skip import single_or_skip
from tests.skip import single_or_first, single_or_skip

DATA = Path(__file__).resolve().parent / "data"


@pytest.mark.parametrize("input_file", ERC7730_EIP712_DESCRIPTORS, ids=path_id)
Expand All @@ -26,3 +33,22 @@ def test_erc7730_registry_files(input_file: Path) -> None:
output_descriptor = convert_and_print_errors(resolved_erc7730_descriptor, ERC7730toEIP712Converter())
output_descriptor = single_or_skip(output_descriptor)
assert_valid_legacy_eip_712(output_descriptor)


@pytest.mark.parametrize("input_file", ERC7730_EIP712_DESCRIPTORS, ids=path_id)
def test_erc7730_registry_files_by_reference(input_file: Path) -> None:
"""
Test converting ERC-7730 => Ledger legacy EIP-712.
Note the test only applies to descriptors with a single contract and message, and only checks output files are
compliant with the Ledger legacy EIP-712 json schema.
"""
reference_path = DATA / input_file.name
if not reference_path.is_file():
pytest.skip(f"No reference file at {reference_path}")
input_erc7730_descriptor = InputERC7730Descriptor.load(input_file)
resolved_erc7730_descriptors = convert_and_print_errors(input_erc7730_descriptor, ERC7730InputToResolved())
resolved_erc7730_descriptor: ResolvedERC7730Descriptor = single_or_first(resolved_erc7730_descriptors)
output_descriptors = convert_and_print_errors(resolved_erc7730_descriptor, ERC7730toEIP712Converter())
output_descriptor: InputEIP712DAppDescriptor = single_or_first(output_descriptors)
assert_dict_equals(dict_from_json_file(reference_path), model_to_json_dict(output_descriptor))
7 changes: 7 additions & 0 deletions tests/skip.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,10 @@ def single_or_skip(value: _T | dict[str, _T] | None) -> _T:
if isinstance(value, dict):
pytest.skip("Multiple descriptors tests not supported")
return value


def single_or_first(value: _T | dict[str, _T] | None) -> _T:
assert value is not None
if isinstance(value, dict):
return next(iter(value.values()))
return value

0 comments on commit f62f2b6

Please sign in to comment.