Repository: ApeWorX/ape

Issue:
Allow `decode=False` when making calls
### Describe

Sometimes, I just easily want to see the returned undecoded calldata from calling contract methods.

### Specification

```python
returndata = contract.getMyComplexValue(decode=False)
```

### Dependencies

_No response_

Discussion:
@linear: <p><a href="https://linear.app/apeworx/issue/APE-1929/allow-decode=false-when-making-calls">APE-1929 Allow `decode=False` when making calls</a></p>

Solution patch:
diff --git a/docs/userguides/contracts.md b/docs/userguides/contracts.md
index e0c1519a5..64747eb5e 100644
--- a/docs/userguides/contracts.md
+++ b/docs/userguides/contracts.md
@@ -294,9 +294,26 @@ contract = Contract("0x...")
 assert contract.get_static_list() == [1, 2, 3]
 ```
 
+By default, Ape decodes returndata into Python types.
+Use `decode=False` to get the raw returndata bytes from a call instead:
+
+```python
+from ape import accounts, Contract
+from eth_pydantic_types import HexBytes
+
+account = accounts.load("<ALIAS>")
+contract = Contract("0x...")
+
+assert contract.get_static_list() == HexBytes(
+    "0x0000000000000000000000000000000000000000000000000000000000000001"
+    "0000000000000000000000000000000000000000000000000000000000000002"
+    "0000000000000000000000000000000000000000000000000000000000000003"
+)
+```
+
 ### Calling Transactions and Transacting Calls
 
-You can treat transactions as calls and vice-versa.
+You can treat transactions as calls and vice versa.
 
 For example, let's say we have a Solidity function:
 
diff --git a/src/ape/contracts/base.py b/src/ape/contracts/base.py
index c6532a214..1cb47a778 100644
--- a/src/ape/contracts/base.py
+++ b/src/ape/contracts/base.py
@@ -119,9 +119,15 @@ class ContractCall(ManagerAccessMixin):
         )
 
     def __call__(self, *args, **kwargs) -> Any:
+        decode_output = kwargs.pop("decode", True)
         txn = self.serialize_transaction(*args, **kwargs)
         txn.chain_id = self.provider.network.chain_id
         raw_output = self.provider.send_call(txn, **kwargs)
+
+        if not decode_output:
+            return raw_output
+
+        # Decode the output bytes into Python types.
         output = self.provider.network.ecosystem.decode_returndata(
             self.abi,
             raw_output,


Test patch:
diff --git a/tests/functional/test_contract_instance.py b/tests/functional/test_contract_instance.py
index 1056284cc..1feca759b 100644
--- a/tests/functional/test_contract_instance.py
+++ b/tests/functional/test_contract_instance.py
@@ -65,6 +65,23 @@ def test_contract_interaction(eth_tester_provider, owner, vyper_contract_instanc
     assert estimate_gas_spy.call_count == 0
 
 
+def test_contract_call(vyper_contract_instance):
+    """
+    By default, calls returndata gets decoded into Python types.
+    """
+    result = vyper_contract_instance.myNumber()
+    assert isinstance(result, int)
+
+
+def test_contract_call_decode_false(vyper_contract_instance):
+    """
+    Use decode=False to get the raw bytes returndata of a function call.
+    """
+    result = vyper_contract_instance.myNumber(decode=False)
+    assert not isinstance(result, int)
+    assert isinstance(result, bytes)
+
+
 def test_eq(vyper_contract_instance, chain):
     other = chain.contracts.instance_at(vyper_contract_instance.address)
     assert other == vyper_contract_instance