Repository: ApeWorX/ape

Issue:
Easier way to get prepped/signed/serialized tx from contract method handle
### Describe

Right now, i have to do this to get tx bytes

```
tx = contract.setNumber.as_transaction(321, sender=user)
prepped_tx = user.prepare_transaction(tx)
signed_tx = user.sign_transaction(prepped_tx)
tx_bytes = signed_tx.serialize_transaction()
```

it is verbose, itd be nice to get them faster and with less lines of code

### Specification

in contract tx/call handler classes, do
```
def serialize_transaction(self, *args, **kwargs):
        contract_transaction = self._as_transaction(*args)
        if "sender" not in kwargs and self.account_manager.default_sender is not None:
            kwargs["sender"] = self.account_manager.default_sender

       # Differs here *****
        return contract_transaction.serialize_transaction(*args, **kwargs)
```

it is very close to `__call__` except `ContactTransactionHandler` calls `ContractTransaction.serialize_transaction` instead of the `ContractTransaction.__call__`

### Dependencies

_No response_

Hints from issue comments:
@linear: <p><a href="https://linear.app/apeworx/issue/APE-1976/easier-way-to-get-preppedsignedserialized-tx-from-contract-method">APE-1976 Easier way to get prepped/signed/serialized tx from contract method handle</a></p>

Solution patch:
diff --git a/src/ape/api/accounts.py b/src/ape/api/accounts.py
index c7bd964e5..7dcb3ea97 100644
--- a/src/ape/api/accounts.py
+++ b/src/ape/api/accounts.py
@@ -83,6 +83,11 @@ class AccountAPI(BaseInterfaceModel, BaseAddress):
         """
         return None
 
+    def prepare_transaction(self, txn: "TransactionAPI", **kwargs) -> "TransactionAPI":
+        sign = kwargs.pop("sign", False)
+        prepared_tx = super().prepare_transaction(txn, **kwargs)
+        return (self.sign_transaction(prepared_tx) or prepared_tx) if sign else prepared_tx
+
     def sign_raw_msghash(self, msghash: "HexBytes") -> Optional[MessageSignature]:
         """
         Sign a raw message hash.
diff --git a/src/ape/api/address.py b/src/ape/api/address.py
index 7dbade917..16620640e 100644
--- a/src/ape/api/address.py
+++ b/src/ape/api/address.py
@@ -189,16 +189,23 @@ class BaseAddress(BaseInterface):
         return self.chain_manager.history[self.address]
 
     def as_transaction(self, **kwargs) -> "TransactionAPI":
+        sign = kwargs.pop("sign", False)
         converted_kwargs = self.conversion_manager.convert_method_kwargs(kwargs)
-        return self.provider.network.ecosystem.create_transaction(
+        tx = self.provider.network.ecosystem.create_transaction(
             receiver=self.address, **converted_kwargs
         )
+        if sender := kwargs.get("sender"):
+            if hasattr(sender, "prepare_transaction"):
+                prepared = sender.prepare_transaction(tx)
+                return (sender.sign_transaction(prepared) or prepared) if sign else prepared
+
+        return tx
 
     def estimate_gas_cost(self, **kwargs) -> int:
         txn = self.as_transaction(**kwargs)
         return self.provider.estimate_gas_cost(txn)
 
-    def prepare_transaction(self, txn: "TransactionAPI") -> "TransactionAPI":
+    def prepare_transaction(self, txn: "TransactionAPI", **kwargs) -> "TransactionAPI":
         """
         Set default values on a transaction.
 
@@ -209,6 +216,7 @@ class BaseAddress(BaseInterface):
 
         Args:
             txn (:class:`~ape.api.transactions.TransactionAPI`): The transaction to prepare.
+            **kwargs: Sub-classes, such as :class:`~ape.api.accounts.AccountAPI`, use additional kwargs.
 
         Returns:
             :class:`~ape.api.transactions.TransactionAPI`
@@ -234,6 +242,7 @@ class BaseAddress(BaseInterface):
                 f"(transfer_value={txn.total_transfer_value}, balance={self.balance})."
             )
 
+        txn.sender = txn.sender or self.address
         return txn
 
 
diff --git a/src/ape/contracts/base.py b/src/ape/contracts/base.py
index 794ca8ce1..e8a9912b9 100644
--- a/src/ape/contracts/base.py
+++ b/src/ape/contracts/base.py
@@ -11,6 +11,7 @@ from eth_pydantic_types import HexBytes
 from eth_utils import to_hex
 from ethpm_types.abi import EventABI
 
+from ape.api.accounts import AccountAPI
 from ape.api.address import Address, BaseAddress
 from ape.api.query import (
     ContractCreation,
@@ -297,12 +298,20 @@ class ContractCallHandler(ContractMethodHandler):
         """
         return self.transact.as_transaction(*args, **kwargs)
 
+    def as_transaction_bytes(self, *args, **txn_kwargs) -> HexBytes:
+        """
+        Get a signed serialized transaction.
+
+        Returns:
+            HexBytes: The serialized transaction
+        """
+        return self.transact.as_transaction_bytes(**txn_kwargs)
+
     @property
     def transact(self) -> "ContractTransactionHandler":
         """
         Send the call as a transaction.
         """
-
         return ContractTransactionHandler(self.contract, self.abis)
 
     def estimate_gas_cost(self, *args, **kwargs) -> int:
@@ -391,12 +400,29 @@ class ContractTransactionHandler(ContractMethodHandler):
         Returns:
             :class:`~ape.api.transactions.TransactionAPI`
         """
-
+        sign = kwargs.pop("sign", False)
         contract_transaction = self._as_transaction(*args)
         transaction = contract_transaction.serialize_transaction(*args, **kwargs)
         self.provider.prepare_transaction(transaction)
+
+        if sender := kwargs.get("sender"):
+            if isinstance(sender, AccountAPI):
+                prepped_tx = sender.prepare_transaction(transaction)
+                return (sender.sign_transaction(prepped_tx) or prepped_tx) if sign else prepped_tx
+
         return transaction
 
+    def as_transaction_bytes(self, *args, **txn_kwargs) -> HexBytes:
+        """
+        Get a signed serialized transaction.
+
+        Returns:
+            HexBytes: The serialized transaction
+        """
+        txn_kwargs["sign"] = True
+        tx = self.as_transaction(*args, **txn_kwargs)
+        return tx.serialize_transaction()
+
     def estimate_gas_cost(self, *args, **kwargs) -> int:
         """
         Get the estimated gas cost (according to the provider) for the


Test patch:
diff --git a/tests/functional/test_accounts.py b/tests/functional/test_accounts.py
index f9d4117bd..723248fcf 100644
--- a/tests/functional/test_accounts.py
+++ b/tests/functional/test_accounts.py
@@ -774,6 +774,20 @@ def test_declare(contract_container, sender):
     assert not receipt.failed
 
 
+def test_prepare_transaction(ethereum, sender):
+    tx = ethereum.create_transaction()
+    prepared_tx = sender.prepare_transaction(tx)
+    assert prepared_tx.sender == sender.address
+    assert prepared_tx.signature is None
+
+
+def test_prepare_transaction_sign(sender, ethereum):
+    tx = ethereum.create_transaction()
+    prepared_tx = sender.prepare_transaction(tx, sign=True)
+    assert prepared_tx.sender == sender.address
+    assert prepared_tx.signature is not None
+
+
 @pytest.mark.parametrize("tx_type", (TransactionType.STATIC, TransactionType.DYNAMIC))
 def test_prepare_transaction_using_auto_gas(sender, ethereum, tx_type):
     params = (
diff --git a/tests/functional/test_contract_instance.py b/tests/functional/test_contract_instance.py
index d1ec93386..737efe65b 100644
--- a/tests/functional/test_contract_instance.py
+++ b/tests/functional/test_contract_instance.py
@@ -1057,6 +1057,19 @@ def test_sending_funds_to_non_payable_constructor_by_accountDeploy(
 def test_as_transaction(tx_type, vyper_contract_instance, owner, eth_tester_provider):
     tx = vyper_contract_instance.setNumber.as_transaction(987, sender=owner, type=tx_type.value)
     assert tx.gas_limit == eth_tester_provider.max_gas
+    assert tx.sender == owner.address
+    assert tx.signature is None
+
+
+def test_as_transaction_sign(vyper_contract_instance, owner, eth_tester_provider):
+    tx = vyper_contract_instance.setNumber.as_transaction(987, sender=owner, sign=True)
+    assert tx.gas_limit == eth_tester_provider.max_gas
+    assert tx.signature is not None
+
+
+def test_as_transaction_bytes(vyper_contract_instance, owner, eth_tester_provider):
+    signed_tx = vyper_contract_instance.setNumber.as_transaction_bytes(987, sender=owner, sign=True)
+    assert isinstance(signed_tx, bytes)
 
 
 @pytest.mark.parametrize(