Repository: vyperlang/vyper

Issue:
Payable methods with Contract Interfaces
I am experimenting with external calls using contract interfaces, and can't seem to figure out payable functions. Adding a test example would be helpful. https://github.com/ethereum/vyper/blob/9eebb7c575545fcf880d815b9c888665dccbea51/tests/parser/features/external_contracts/test_external_contract_calls.py#L1

This works: 

```python
# FACTORY CONTRACT
class Exchange():
    def setup(token_addr: address) -> bool: pass

@public
def launch_exchange(token_addr: address) -> address:
    exchange_addr: address = create_with_code_of(self.exchange_template)
    assert Exchange(exchange_addr).setup(token_addr) == True

# EXCHANGE CONTRACT
@public
def setup(token_addr: address) -> bool:
    self.token_address = token_addr
    return True
```
I tried making the functions payable, and changing the call to 
```python
assert Exchange(exchange_addr).setup(token_addr, value=msg.value) == True
```
and the function still executes, but no ETH is transferred. I'm wondering if this is possible, and what the proper syntax is.


#### EDIT: forgot cute animal picture

![image](https://user-images.githubusercontent.com/9343483/39498064-1c2a2eee-4d74-11e8-8b52-680dadef2bce.png)



Discussion:
Looking at the source code, it seems like external contracts always has their wei value set to zero:
https://github.com/ethereum/vyper/blob/master/vyper/parser/parser.py#L534
Will have to add a parameter to support this. `value=` kwarg seems natural.
@jacqueswww would we support any other calling parameters?
We could, only other one is gas right?
I believe in Solidity gas and value are the only two additional parameters for calls using interfaces. 
That's what I thought, just wanted to make sure I wasn't forgetting anything

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -22,7 +22,7 @@
     url='https://github.com/ethereum/vyper',
     license=license,
     packages=find_packages(exclude=('tests', 'docs')),
-    install_requires=['py-evm>=0.2.0a12'],
+    install_requires=['py-evm==0.2.0a12'],
     setup_requires=['pytest-runner'],
     python_requires='>=3.6',
     tests_require=['pytest', 'pytest-cov', 'ethereum==2.3.1'],
diff --git a/vyper/parser/expr.py b/vyper/parser/expr.py
--- a/vyper/parser/expr.py
+++ b/vyper/parser/expr.py
@@ -440,6 +440,17 @@ def unary_operations(self):
         else:
             raise StructureException("Only the 'not' unary operator is supported")
 
+    def _get_external_contract_keywords(self):
+        value, gas = None, None
+        for kw in self.expr.keywords:
+            if kw.arg not in ('value', 'gas'):
+                raise TypeMismatchException('Invalid keyword argument, only "gas" and "value" supported.', self.expr)
+            elif kw.arg == 'gas':
+                gas = Expr.parse_value_expr(kw.value, self.context)
+            elif kw.arg == 'value':
+                value = Expr.parse_value_expr(kw.value, self.context)
+        return value, gas
+
     # Function calls
     def call(self):
         from .parser import (
@@ -494,17 +505,20 @@ def call(self):
         elif isinstance(self.expr.func, ast.Attribute) and isinstance(self.expr.func.value, ast.Call):
             contract_name = self.expr.func.value.func.id
             contract_address = Expr.parse_value_expr(self.expr.func.value.args[0], self.context)
-            return external_contract_call(self.expr, self.context, contract_name, contract_address, True, pos=getpos(self.expr))
+            value, gas = self._get_external_contract_keywords()
+            return external_contract_call(self.expr, self.context, contract_name, contract_address, True, pos=getpos(self.expr), value=value, gas=gas)
         elif isinstance(self.expr.func.value, ast.Attribute) and self.expr.func.value.attr in self.context.sigs:
             contract_name = self.expr.func.value.attr
             var = self.context.globals[self.expr.func.value.attr]
             contract_address = unwrap_location(LLLnode.from_list(var.pos, typ=var.typ, location='storage', pos=getpos(self.expr), annotation='self.' + self.expr.func.value.attr))
-            return external_contract_call(self.expr, self.context, contract_name, contract_address, True, pos=getpos(self.expr))
+            value, gas = self._get_external_contract_keywords()
+            return external_contract_call(self.expr, self.context, contract_name, contract_address, True, pos=getpos(self.expr), value=value, gas=gas)
         elif isinstance(self.expr.func.value, ast.Attribute) and self.expr.func.value.attr in self.context.globals:
             contract_name = self.context.globals[self.expr.func.value.attr].typ.unit
             var = self.context.globals[self.expr.func.value.attr]
             contract_address = unwrap_location(LLLnode.from_list(var.pos, typ=var.typ, location='storage', pos=getpos(self.expr), annotation='self.' + self.expr.func.value.attr))
-            return external_contract_call(self.expr, self.context, contract_name, contract_address, var.modifiable, pos=getpos(self.expr))
+            value, gas = self._get_external_contract_keywords()
+            return external_contract_call(self.expr, self.context, contract_name, contract_address, var.modifiable, pos=getpos(self.expr), value=value, gas=gas)
         else:
             raise StructureException("Unsupported operator: %r" % ast.dump(self.expr), self.expr)
 
diff --git a/vyper/parser/parser.py b/vyper/parser/parser.py
--- a/vyper/parser/parser.py
+++ b/vyper/parser/parser.py
@@ -516,7 +516,11 @@ def parse_body(code, context):
     return LLLnode.from_list(['seq'] + o, pos=getpos(code[0]) if code else None)
 
 
-def external_contract_call(node, context, contract_name, contract_address, is_modifiable, pos):
+def external_contract_call(node, context, contract_name, contract_address, is_modifiable, pos, value=None, gas=None):
+    if value is None:
+        value = 0
+    if gas is None:
+        gas = 'gas'
     if contract_name not in context.sigs:
         raise VariableDeclarationException("Contract not declared yet: %s" % contract_name)
     method_name = node.func.attr
@@ -529,9 +533,9 @@ def external_contract_call(node, context, contract_name, contract_address, is_mo
     sub = ['seq', ['assert', ['extcodesize', contract_address]],
                     ['assert', ['ne', 'address', contract_address]]]
     if context.is_constant or not is_modifiable:
-        sub.append(['assert', ['staticcall', 'gas', contract_address, inargs, inargsize, output_placeholder, output_size]])
+        sub.append(['assert', ['staticcall', gas, contract_address, inargs, inargsize, output_placeholder, output_size]])
     else:
-        sub.append(['assert', ['call', 'gas', contract_address, 0, inargs, inargsize, output_placeholder, output_size]])
+        sub.append(['assert', ['call', gas, contract_address, value, inargs, inargsize, output_placeholder, output_size]])
     sub.extend(returner)
     o = LLLnode.from_list(sub, typ=sig.output_type, location='memory', pos=getpos(node))
     return o


Test patch:
diff --git a/tests/parser/features/external_contracts/test_external_contract_calls.py b/tests/parser/features/external_contracts/test_external_contract_calls.py
--- a/tests/parser/features/external_contracts/test_external_contract_calls.py
+++ b/tests/parser/features/external_contracts/test_external_contract_calls.py
@@ -1,4 +1,9 @@
-from vyper.exceptions import StructureException, VariableDeclarationException, InvalidTypeException
+from vyper.exceptions import (
+    StructureException,
+    VariableDeclarationException,
+    InvalidTypeException,
+    TypeMismatchException
+)
 
 
 def test_external_contract_calls(get_contract, get_contract_with_gas_estimation):
@@ -540,3 +545,106 @@ def foo(contract_address: contract(Bar)) -> int128:
     """
 
     assert_compile_failed(lambda: get_contract(contract_1), InvalidTypeException)
+
+
+def test_external_with_payble_value(chain, get_contract_with_gas_estimation):
+    contract_1 = """
+@payable
+@public
+def get_lucky() -> int128:
+    return 1
+
+@public
+def get_balance() -> int128(wei):
+    return self.balance
+"""
+
+    contract_2 = """
+class Bar():
+    def set_lucky(arg1: int128): pass
+    def get_lucky() -> int128: pass
+
+bar_contract: modifiable(Bar)
+
+@public
+def set_contract(contract_address: contract(Bar)):
+    self.bar_contract = contract_address
+
+@payable
+@public
+def get_lucky(amount_to_send: int128) -> int128:
+    if amount_to_send != 0:
+        return self.bar_contract.get_lucky(value=amount_to_send)
+    else: # send it all
+        return self.bar_contract.get_lucky(value=msg.value)
+"""
+
+    c1 = get_contract_with_gas_estimation(contract_1)
+    c2 = get_contract_with_gas_estimation(contract_2)
+
+    # Set address.
+    assert c1.get_lucky() == 1
+    assert c1.get_balance() == 0
+    c2.set_contract(c1.address)
+
+    # Send some eth
+    assert c2.get_lucky(value=500) == 1
+    # Contract 1 received money.
+    assert c1.get_balance() == 500
+    assert chain.head_state.get_balance(c1.address) == 500
+    assert chain.head_state.get_balance(c2.address) == 0
+
+    # Send subset of amount
+    assert c2.get_lucky(250, value=500) == 1
+    # Contract 1 received more money.
+    assert c1.get_balance() == 750
+    assert chain.head_state.get_balance(c1.address) == 750
+    assert chain.head_state.get_balance(c2.address) == 250
+
+
+def test_external_call_with_gas(assert_tx_failed, get_contract_with_gas_estimation):
+    contract_1 = """
+@public
+def get_lucky() -> int128:
+    return 656598
+"""
+
+    contract_2 = """
+class Bar():
+    def set_lucky(arg1: int128): pass
+    def get_lucky() -> int128: pass
+
+bar_contract: modifiable(Bar)
+
+@public
+def set_contract(contract_address: contract(Bar)):
+    self.bar_contract = contract_address
+
+@public
+def get_lucky(gas_amount: int128) -> int128:
+    return self.bar_contract.get_lucky(gas=gas_amount)
+"""
+
+    c1 = get_contract_with_gas_estimation(contract_1)
+    c2 = get_contract_with_gas_estimation(contract_2)
+    c2.set_contract(c1.address)
+
+    assert c2.get_lucky(1000) == 656598
+    assert_tx_failed(lambda: c2.get_lucky(100))  # too little gas.
+
+
+def test_invalid_keyword_on_call(assert_compile_failed, get_contract_with_gas_estimation):
+
+    contract_1 = """
+class Bar():
+    def set_lucky(arg1: int128): pass
+    def get_lucky() -> int128: pass
+
+bar_contract: modifiable(Bar)
+
+@public
+def get_lucky(amount_to_send: int128) -> int128:
+    return self.bar_contract.get_lucky(gass=1)
+    """
+
+    assert_compile_failed(lambda: get_contract_with_gas_estimation(contract_1), TypeMismatchException)