Skip to content

Commit f866687

Browse files
committed
feat: support loading types from ethPM Manifest
1 parent 82f44ee commit f866687

File tree

4 files changed

+106
-7
lines changed

4 files changed

+106
-7
lines changed

tests/cli/vyper_json/test_interfaces.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,17 @@ def test_interfaces_output():
8383
}
8484

8585

86+
def test_manifest_output():
87+
input_json = {
88+
"interfaces": {
89+
"/bar.json": {"contractTypes": {"Bar": {"abi": BAR_ABI}}},
90+
}
91+
}
92+
result = get_input_dict_interfaces(input_json)
93+
assert isinstance(result, dict)
94+
assert result == {"Bar": {"type": "json", "code": BAR_ABI}}
95+
96+
8697
# get_interface_codes tests
8798

8899

vyper/cli/utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,20 @@
99
def get_interface_file_path(base_paths: Sequence, import_path: str) -> Path:
1010
relative_path = Path(import_path)
1111
for path in base_paths:
12+
# Find ABI JSON files
1213
file_path = path.joinpath(relative_path)
1314
suffix = next((i for i in (".vy", ".json") if file_path.with_suffix(i).exists()), None)
1415
if suffix:
1516
return file_path.with_suffix(suffix)
17+
18+
# Find ethPM Manifest files (`from path.to.Manifest import InterfaceName`)
19+
# NOTE: Use file parent because this assumes that `file_path`
20+
# coincides with an ABI interface file
21+
file_path = file_path.parent
22+
suffix = next((i for i in (".vy", ".json") if file_path.with_suffix(i).exists()), None)
23+
if suffix:
24+
return file_path.with_suffix(suffix)
25+
1626
raise FileNotFoundError(f" Cannot locate interface '{import_path}{{.vy,.json}}'")
1727

1828

vyper/cli/vyper_compile.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,36 @@ def get_interface_codes(root_path: Path, contract_sources: ContractCodes) -> Dic
217217
with valid_path.open() as fh:
218218
code = fh.read()
219219
if valid_path.suffix == ".json":
220-
interfaces[file_path][interface_name] = {
221-
"type": "json",
222-
"code": json.loads(code.encode()),
223-
}
220+
contents = json.loads(code.encode())
221+
222+
# EthPM Manifest (EIP-2678)
223+
if "contractTypes" in contents:
224+
if (
225+
interface_name not in contents["contractTypes"]
226+
or "abi" not in contents["contractTypes"][interface_name]
227+
):
228+
raise ValueError(
229+
f"Could not find interface '{interface_name}'"
230+
f" in manifest '{valid_path}'."
231+
)
232+
233+
interfaces[file_path][interface_name] = {
234+
"type": "json",
235+
"code": contents["contractTypes"][interface_name]["abi"],
236+
}
237+
238+
# ABI JSON file (either `List[ABI]` or `{"abi": List[ABI]}`)
239+
elif isinstance(contents, list) or (
240+
"abi" in contents and isinstance(contents["abi"], list)
241+
):
242+
interfaces[file_path][interface_name] = {
243+
"type": "json",
244+
"code": contents,
245+
}
246+
247+
else:
248+
raise ValueError(f"Corrupted file: '{valid_path}'")
249+
224250
else:
225251
interfaces[file_path][interface_name] = {"type": "vyper", "code": code}
226252

vyper/cli/vyper_json.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,22 +182,47 @@ def get_input_dict_contracts(input_dict: Dict) -> ContractCodes:
182182

183183
def get_input_dict_interfaces(input_dict: Dict) -> Dict:
184184
interface_sources: Dict = {}
185+
185186
for path, value in input_dict.get("interfaces", {}).items():
186187
key = _standardize_path(path)
188+
187189
if key.endswith(".json"):
188-
if "abi" not in value:
190+
# EthPM Manifest v3 (EIP-2678)
191+
if "contractTypes" in value:
192+
for name, ct in value["contractTypes"].items():
193+
if name in interface_sources:
194+
raise JSONError(f"Interface namespace collision: {name}")
195+
196+
interface_sources[name] = {"type": "json", "code": ct["abi"]}
197+
198+
continue # Skip to next interface
199+
200+
# ABI JSON file (`{"abi": List[ABI]}`)
201+
elif "abi" in value:
202+
interface = {"type": "json", "code": value["abi"]}
203+
204+
# ABI JSON file (`List[ABI]`)
205+
elif isinstance(value, list):
206+
interface = {"type": "json", "code": value}
207+
208+
else:
189209
raise JSONError(f"Interface '{path}' must have 'abi' field")
190-
interface = {"type": "json", "code": value["abi"]}
210+
191211
elif key.endswith(".vy"):
192212
if "content" not in value:
193213
raise JSONError(f"Interface '{path}' must have 'content' field")
214+
194215
interface = {"type": "vyper", "code": value["content"]}
216+
195217
else:
196218
raise JSONError(f"Interface '{path}' must have suffix '.vy' or '.json'")
219+
197220
key = key.rsplit(".", maxsplit=1)[0]
198221
if key in interface_sources:
199222
raise JSONError(f"Interface namespace collision: {key}")
223+
200224
interface_sources[key] = interface
225+
201226
return interface_sources
202227

203228

@@ -248,6 +273,11 @@ def get_interface_codes(
248273
code = contract_sources[contract_path]
249274
interface_codes = extract_file_interface_imports(code)
250275
for interface_name, interface_path in interface_codes.items():
276+
# If we know the interfaces already (e.g. EthPM Manifest file)
277+
if interface_name in interface_sources:
278+
interfaces[interface_name] = interface_sources[interface_name]
279+
continue
280+
251281
path = Path(contract_path).parent.joinpath(interface_path).as_posix()
252282
keys = [_standardize_path(path)]
253283
if not interface_path.startswith("."):
@@ -281,7 +311,29 @@ def get_interface_codes(
281311
with valid_path.open() as fh:
282312
code = fh.read()
283313
if valid_path.suffix == ".json":
284-
interfaces[interface_name] = {"type": "json", "code": json.loads(code.encode())}
314+
code_dict = json.loads(code.encode())
315+
# EthPM Manifest v3 (EIP-2678)
316+
if "contractTypes" in code_dict:
317+
if interface_name not in code_dict["contractTypes"]:
318+
raise JSONError(f"'{interface_name}' not found in '{valid_path}'")
319+
320+
if "abi" not in code_dict["contractTypes"][interface_name]:
321+
raise JSONError(f"Missing abi for '{interface_name}' in '{valid_path}'")
322+
323+
abi = code_dict["contractTypes"][interface_name]["abi"]
324+
interfaces[interface_name] = {"type": "json", "code": abi}
325+
326+
# ABI JSON (`{"abi": List[ABI]}`)
327+
elif "abi" in code_dict:
328+
interfaces[interface_name] = {"type": "json", "code": code_dict["abi"]}
329+
330+
# ABI JSON (`List[ABI]`)
331+
elif isinstance(code_dict, list):
332+
interfaces[interface_name] = {"type": "json", "code": code_dict}
333+
334+
else:
335+
raise JSONError(f"Unexpected type in file: '{valid_path}'")
336+
285337
else:
286338
interfaces[interface_name] = {"type": "vyper", "code": code}
287339

0 commit comments

Comments
 (0)