Repository: vyperlang/vyper

Issue:
VIP: Make the type argument in method_id optional
## Simple Summary
`method_id`'s second argument (the output type) should be optional with a default of `bytes[4]`.


## Motivation
* Generating a 4 byte function selector is by far the most common use-case for this method
* The 32 byte flavor is just as easily achieved using `keccak256`

## Specification
```python
method_id(method, output_type=bytes[4]) → Union[bytes[4], bytes32]
```

`output_type` may still be given as a positional arg so that this is not a breaking change.

## Backwards Compatibility
No issues.


## Copyright
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/)



Discussion:
I was thinking to remove `output_type` entirely, and figure out `bytes[4]` or `bytes32` based on context (calls or events)
That sounds pretty magical.
Not really, it should be obvious because it is only useful as `bytes[4]` for calls, and only useful as `bytes32` for events. I don't think the user needs to know this detail.

Resolving patch:
diff --git a/vyper/functions/functions.py b/vyper/functions/functions.py
--- a/vyper/functions/functions.py
+++ b/vyper/functions/functions.py
@@ -645,27 +645,27 @@ def build_LLL(self, expr, args, kwargs, context):
 class MethodID:
 
     _id = "method_id"
-    _inputs = [("method", "str_literal"), ("type", "name_literal")]
 
     def evaluate(self, node):
-        validate_call_args(node, 2)
+        validate_call_args(node, 1, ["output_type"])
 
         args = node.args
         if not isinstance(args[0], vy_ast.Str):
             raise InvalidType("method id must be given as a literal string", args[0])
         if " " in args[0].value:
-            raise InvalidLiteral("Invalid function signature no spaces allowed.")
-
-        if isinstance(args[1], vy_ast.Name) and args[1].id == "bytes32":
-            length = 32
-        elif (
-            isinstance(args[1], vy_ast.Subscript)
-            and args[1].get("value.id") == "bytes"
-            and args[1].get("slice.value.value") == 4
-        ):
-            length = 4
+            raise InvalidLiteral("Invalid function signature - no spaces allowed.")
+
+        if node.keywords:
+            return_type = get_type_from_annotation(node.keywords[0].value, DataLocation.UNSET)
+            if isinstance(return_type, Bytes32Definition):
+                length = 32
+            elif isinstance(return_type, BytesArrayDefinition) and return_type.length == 4:
+                length = 4
+            else:
+                raise ArgumentException("output_type must be bytes[4] or bytes32", node.keywords[0])
         else:
-            raise InvalidType("Can only produce bytes32 or bytes[4] as outputs", args[1])
+            # if `output_type` is not given, default to `bytes[4]`
+            length = 4
 
         method_id = fourbytes_to_int(keccak256(args[0].value.encode())[:4])
         value = method_id.to_bytes(length, "big")


Test patch:
diff --git a/tests/examples/tokens/test_erc721.py b/tests/examples/tokens/test_erc721.py
--- a/tests/examples/tokens/test_erc721.py
+++ b/tests/examples/tokens/test_erc721.py
@@ -248,7 +248,7 @@ def onERC721Received(
         _tokenId: uint256,
         _data: bytes[1024]
     ) -> bytes32:
-    return method_id("onERC721Received(address,address,uint256,bytes)", bytes32)
+    return method_id("onERC721Received(address,address,uint256,bytes)", output_type=bytes32)
     """
     )
     tx_hash = c.safeTransferFrom(
diff --git a/tests/parser/functions/test_method_id.py b/tests/parser/functions/test_method_id.py
--- a/tests/parser/functions/test_method_id.py
+++ b/tests/parser/functions/test_method_id.py
@@ -6,35 +6,44 @@ def double(x: int128) -> int128:
 
 @public
 def returnten() -> int128:
-    ans: bytes[32] = raw_call(self, concat(method_id("double(int128)", bytes[4]), convert(5, bytes32)), gas=50000, max_outsize=32)  # noqa: E501
+    ans: bytes[32] = raw_call(self, concat(method_id("double(int128)"), convert(5, bytes32)), gas=50000, max_outsize=32)  # noqa: E501
     return convert(convert(ans, bytes32), int128)
     """
     c = get_contract_with_gas_estimation(method_id_test)
     assert c.returnten() == 10
-    print("Passed method ID test")
 
 
-# Disabled because existing codebase does not allow bytes32 to be represented as
-# a bytestring - enable again once typechecking is refactored out of parser/stmt.py
-
-# def test_method_id_bytes32(get_contract):
-#     code = """
-# @public
-# def sig() -> bytes32:
-#     return method_id('transfer(address,uint256)', bytes32)
-#     """
-#     c = get_contract(code)
-#     sig = c.sig()
+def test_method_id_bytes32(get_contract):
+    code = """
+@public
+def sig() -> bytes32:
+    return method_id('transfer(address,uint256)', output_type=bytes32)
+    """
+    c = get_contract(code)
+    sig = c.sig()
 
-#     assert len(sig) == 32
-#     assert sig[-4:] == b"\xa9\x05\x9c\xbb"
+    assert len(sig) == 32
+    assert sig[-4:] == b"\xa9\x05\x9c\xbb"
 
 
 def test_method_id_bytes4(get_contract):
     code = """
 @public
 def sig() -> bytes[4]:
-    return method_id('transfer(address,uint256)', bytes[4])
+    return method_id('transfer(address,uint256)', output_type=bytes[4])
+    """
+    c = get_contract(code)
+    sig = c.sig()
+
+    # assert len(sig) == 4
+    assert sig == b"\xa9\x05\x9c\xbb"
+
+
+def test_method_id_bytes4_default(get_contract):
+    code = """
+@public
+def sig() -> bytes[4]:
+    return method_id('transfer(address,uint256)')
     """
     c = get_contract(code)
     sig = c.sig()
@@ -47,7 +56,7 @@ def test_method_id_invalid_space(get_contract, assert_compile_failed):
     code = """
 @public
 def sig() -> bytes32:
-    return method_id('transfer(address, uint256)', bytes32)
+    return method_id('transfer(address, uint256)', output_type=bytes32)
     """
     assert_compile_failed(lambda: get_contract(code))
 
@@ -56,6 +65,6 @@ def test_method_id_invalid_type(get_contract, assert_compile_failed):
     code = """
 @public
 def sig() -> int128:
-    return method_id('transfer(address,uint256)', int128)
+    return method_id('transfer(address,uint256)', output_type=int128)
     """
     assert_compile_failed(lambda: get_contract(code))
diff --git a/tests/parser/functions/test_raw_call.py b/tests/parser/functions/test_raw_call.py
--- a/tests/parser/functions/test_raw_call.py
+++ b/tests/parser/functions/test_raw_call.py
@@ -130,7 +130,7 @@ def __init__(_owner_setter: address):
 @public
 def set(i: int128, owner: address):
     # delegate setting owners to other contract.s
-    cdata: bytes[68] = concat(method_id("set_owner(int128,address)", bytes[4]), convert(i, bytes32), convert(owner, bytes32))  # noqa: E501
+    cdata: bytes[68] = concat(method_id("set_owner(int128,address)"), convert(i, bytes32), convert(owner, bytes32))  # noqa: E501
     raw_call(
         self.owner_setter_contract,
         cdata,
@@ -172,7 +172,7 @@ def foo(_bar: bytes32):
 @public
 def foo_call(_addr: address):
     cdata: bytes[40] = concat(
-        method_id("foo(bytes32)", bytes[4]),
+        method_id("foo(bytes32)"),
         0x0000000000000000000000000000000000000000000000000000000000000001
     )
     raw_call(_addr, cdata, max_outsize=0{})
@@ -206,7 +206,7 @@ def foo() -> int128:
 def foo(_addr: address) -> int128:
     _response: bytes[32] = raw_call(
         _addr,
-        method_id("foo()", bytes[4]),
+        method_id("foo()"),
         max_outsize=32,
         is_static_call=True,
     )
@@ -236,7 +236,7 @@ def foo() -> int128:
 def foo(_addr: address) -> int128:
     _response: bytes[32] = raw_call(
         _addr,
-        method_id("foo()", bytes[4]),
+        method_id("foo()"),
         max_outsize=32,
         is_static_call=True,
     )
@@ -255,7 +255,7 @@ def foo(_addr: address) -> int128:
 @public
 @constant
 def foo(_addr: address):
-    raw_call(_addr, method_id("foo()", bytes[4]))
+    raw_call(_addr, method_id("foo()"))
     """,
         ConstancyViolation,
     ),
@@ -263,7 +263,7 @@ def foo(_addr: address):
         """
 @public
 def foo(_addr: address):
-    raw_call(_addr, method_id("foo()", bytes[4]), is_delegate_call=True, is_static_call=True)
+    raw_call(_addr, method_id("foo()"), is_delegate_call=True, is_static_call=True)
     """,
         ArgumentException,
     ),