Repository: ethereum/web3.py

Issue:
sign_and_send_raw_middleware doesn't work with async provider
* Version: 6.0.0b9
* Python: 3.10
* OS: maxOS

### What was wrong?

`construct_sign_and_send_raw_middleware` returns not awaitable middleware that causes transaction submission to fail.

### How can it be fixed?

Here is a overridden middleware that works with async provider:

```python
from typing import Any, Callable, Collection, Union, cast

# pylint: disable=no-name-in-module
from cytoolz.dicttoolz import assoc
from eth_account.signers.local import LocalAccount
from eth_keys.datatypes import PrivateKey
from eth_typing import ChecksumAddress, HexStr
from eth_utils.toolz import curry
from web3 import Web3
from web3._utils.async_transactions import fill_transaction_defaults
from web3.middleware.signing import format_transaction, gen_normalized_accounts
from web3.types import AsyncMiddleware, Middleware, RPCEndpoint, RPCResponse, TxParams

_PrivateKey = Union[LocalAccount, PrivateKey, HexStr, bytes]


@curry
async def fill_nonce(w3: 'Web3', transaction: TxParams) -> TxParams:
    if 'from' in transaction and 'nonce' not in transaction:
        return assoc(
            transaction,
            'nonce',
            await w3.eth.get_transaction_count(  # type: ignore
                cast(ChecksumAddress, transaction['from'])
            ),
        )
    return transaction


def construct_async_sign_and_send_raw_middleware(
    private_key_or_account: Union[_PrivateKey, Collection[_PrivateKey]]
) -> Middleware:
    """Capture transactions sign and send as raw transactions


    Keyword arguments:
    private_key_or_account -- A single private key or a tuple,
    list or set of private keys. Keys can be any of the following formats:
      - An eth_account.LocalAccount object
      - An eth_keys.PrivateKey object
      - A raw private key as a hex string or byte string
    """

    accounts = gen_normalized_accounts(private_key_or_account)

    async def sign_and_send_raw_middleware(
        make_request: Callable[[RPCEndpoint, Any], Any], _async_w3: 'Web3'
    ) -> AsyncMiddleware:

        async def middleware(method: RPCEndpoint, params: Any) -> RPCResponse:
            if method != 'eth_sendTransaction':
                return await make_request(method, params)

            transaction = await fill_nonce(_async_w3, params[0])
            transaction = await fill_transaction_defaults(_async_w3, transaction)
            transaction = format_transaction(transaction)

            if 'from' not in transaction:
                return await make_request(method, params)

            if transaction.get('from') not in accounts:
                return await make_request(method, params)

            # pylint: disable=unsubscriptable-object
            account = accounts[transaction['from']]
            raw_tx = account.sign_transaction(transaction).rawTransaction

            return await make_request(RPCEndpoint('eth_sendRawTransaction'), [Web3.to_hex(raw_tx)])

        return middleware

    return sign_and_send_raw_middleware
```


Discussion:

Patch:
diff --git a/docs/ens_overview.rst b/docs/ens_overview.rst
index 654260b4..77db889d 100644
--- a/docs/ens_overview.rst
+++ b/docs/ens_overview.rst
@@ -188,8 +188,8 @@ Get the Address for an ENS Name
 .. code-block:: python
 
     from ens.auto import ns
-    eth_address = ns.address('jasoncarver.eth')
-    assert eth_address == '0x582AC4D8929f58c217d4a52aDD361AE470a8a4cD'
+    eth_address = ns.address('ens.eth')
+    assert eth_address == '0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7'
 
 The ``ENS`` module has no opinion as to which TLD you can use,
 but will not infer a TLD if it is not provided with the name.
@@ -199,13 +199,13 @@ Get the ENS Name for an Address
 
 .. code-block:: python
 
-    domain = ns.name('0x582AC4D8929f58c217d4a52aDD361AE470a8a4cD')
+    domain = ns.name('0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7')
 
     # name() also accepts the bytes version of the address
-    assert ns.name(b'[ c$o!\x91\xf1\x8f&u\xce\xdb\x8b(\x10.\x95tX') == domain
+    assert ns.name(b'\xfe\x89\xccz\xbb,A\x83h:\xb7\x16S\xc4\xcd\xc9\xb0-D\xb7') == domain
 
     # confirm that the name resolves back to the address that you looked up:
-    assert ns.address(domain) == '0x582AC4D8929f58c217d4a52aDD361AE470a8a4cD'
+    assert ns.address(domain) == '0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7'
 
 .. note:: For accuracy, and as a recommendation from the ENS documentation on
     `reverse resolution <https://docs.ens.domains/dapp-developer-guide/resolving-names#reverse-resolution>`_,
@@ -233,24 +233,24 @@ you must already be the owner of the domain (or its parent).
 
 .. code-block:: python
 
-    ns.setup_address('jasoncarver.eth', '0x582AC4D8929f58c217d4a52aDD361AE470a8a4cD')
+    ns.setup_address('ens.eth', '0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7')
 
 In the common case where you want to point the name to the owning address, you can skip the address.
 
 .. code-block:: python
 
-    ns.setup_address('jasoncarver.eth')
+    ns.setup_address('ens.eth')
 
 You can claim arbitrarily deep subdomains.
 
 .. code-block:: python
 
-    ns.setup_address('supreme.executive.power.derives.from.a.mandate.from.the.masses.jasoncarver.eth')
+    ns.setup_address('supreme.executive.power.derives.from.a.mandate.from.the.masses.ens.eth')
 
     # wait for the transaction to be mined, then:
     assert (
-        ns.address('supreme.executive.power.derives.from.a.mandate.from.the.masses.jasoncarver.eth')
-        == '0x582AC4D8929f58c217d4a52aDD361AE470a8a4cD'
+        ns.address('supreme.executive.power.derives.from.a.mandate.from.the.masses.ens.eth')
+        == '0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7'
     )
 
 .. warning:: Gas costs scale up with the number of subdomains!
@@ -265,14 +265,14 @@ this is referred to as "reverse" resolution. The ENS Reverse Resolver is used fo
 
 .. code-block:: python
 
-    ns.setup_name('jasoncarver.eth', '0x582AC4D8929f58c217d4a52aDD361AE470a8a4cD')
+    ns.setup_name('ens.eth', '0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7')
 
 If you don't supply the address, :meth:`~ens.ENS.setup_name` will assume you want the
 address returned by :meth:`~ens.ENS.address`.
 
 .. code-block:: python
 
-    ns.setup_name('jasoncarver.eth')
+    ns.setup_name('ens.eth')
 
 If the name doesn't already point to an address, :meth:`~ens.ENS.setup_name` will
 call :meth:`~ens.ENS.setup_address` for you.
@@ -281,7 +281,7 @@ Wait for the transaction to be mined, then:
 
 .. code-block:: python
 
-    assert ns.name('0x582AC4D8929f58c217d4a52aDD361AE470a8a4cD') == 'jasoncarver.eth'
+    assert ns.name('0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7') == 'ens.eth'
 
 ....
 
@@ -298,15 +298,15 @@ You'll need to setup the address first, and then the text can be set:
 
 .. code-block:: python
 
-    ns.setup_address('jasoncarver.eth', '0x582AC4D8929f58c217d4a52aDD361AE470a8a4cD')
-    ns.set_text('jasoncarver.eth', 'url', 'https://example.com')
+    ns.setup_address('ens.eth', '0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7')
+    ns.set_text('ens.eth', 'url', 'https://example.com')
 
 A transaction dictionary can be passed as the last argument if desired:
 
 .. code-block:: python
 
     transaction_dict = {'from': '0x123...'}
-    ns.set_text('jasoncarver.eth', 'url', 'https://example.com', transaction_dict)
+    ns.set_text('ens.eth', 'url', 'https://example.com', transaction_dict)
 
 If the transaction dictionary is not passed, sensible defaults will be used, and if
 a transaction dictionary is passed but does not have a ``from`` value,
@@ -319,7 +319,7 @@ Anyone can read the data from an ENS Record:
 
 .. code-block:: python
 
-    url = ns.get_text('jasoncarver.eth', 'url')
+    url = ns.get_text('ens.eth', 'url')
     assert url == 'https://example.com'
 
 ....
@@ -334,7 +334,7 @@ You can get the resolver for an ENS name via the :meth:`~ens.ENS.resolver` metho
 
 .. code-block:: python
 
-    >>> resolver = ns.resolver('jasoncarver.eth')
+    >>> resolver = ns.resolver('ens.eth')
     >>> resolver.address
     '0x5B2063246F2191f18F2675ceDB8b28102e957458'
 
diff --git a/docs/middleware.rst b/docs/middleware.rst
index 430f960e..3cfcb7bb 100644
--- a/docs/middleware.rst
+++ b/docs/middleware.rst
@@ -537,6 +537,7 @@ Signing
 ~~~~~~~
 
 .. py:method:: web3.middleware.construct_sign_and_send_raw_middleware(private_key_or_account)
+               web3.middleware.async_construct_sign_and_send_raw_middleware(private_key_or_account)
 
 This middleware automatically captures transactions, signs them, and sends them as raw transactions.
 The ``from`` field on the transaction, or ``w3.eth.default_account`` must be set to the address of the private key for
@@ -573,6 +574,27 @@ this middleware to have any effect.
     >>> w3.middleware_onion.add(construct_sign_and_send_raw_middleware(acct))
     >>> w3.eth.default_account = acct.address
 
+    >>> # use `eth_sendTransaction` to automatically sign and send the raw transaction
+    >>> w3.eth.send_transaction(tx_dict)
+    HexBytes('0x09511acf75918fd03de58141d2fd409af4fd6d3dce48eb3aa1656c8f3c2c5c21')
+
+Similarly, with AsyncWeb3:
+
+.. code-block:: python
+
+    >>> from web3 import AsyncWeb3
+    >>> async_w3 = AsyncWeb3(AsyncHTTPProvider('HTTP_ENDPOINT'))
+    >>> from web3.middleware import async_construct_sign_and_send_raw_middleware
+    >>> from eth_account import Account
+    >>> import os
+    >>> acct = async_w3.eth.account.from_key(os.environ.get('PRIVATE_KEY'))
+    >>> async_w3.middleware_onion.add(await async_construct_sign_and_send_raw_middleware(acct))
+    >>> async_w3.eth.default_account = acct.address
+
+    >>> # use `eth_sendTransaction` to automatically sign and send the raw transaction
+    >>> await async_w3.eth.send_transaction(tx_dict)
+    HexBytes('0x09511acf75918fd03de58141d2fd409af4fd6d3dce48eb3aa1656c8f3c2c5c21')
+
 Now you can send a transaction from acct.address without having to build and sign each raw transaction.
 
 When making use of this signing middleware, when sending dynamic fee transactions (recommended over legacy transactions),
diff --git a/newsfragments/2936.bugfix.rst b/newsfragments/2936.bugfix.rst
new file mode 100644
index 00000000..1079499c
--- /dev/null
+++ b/newsfragments/2936.bugfix.rst
@@ -0,0 +1,1 @@
+Fix for a possible bug in ``construct_sign_and_send_raw_middleware`` where the signed transaction was sent as bytes and expected to be converted to hex by formatting later on. It is now explicitly sent as the hex string hash within the middleware.
diff --git a/newsfragments/3020.docs.rst b/newsfragments/3020.docs.rst
new file mode 100644
index 00000000..4842592d
--- /dev/null
+++ b/newsfragments/3020.docs.rst
@@ -0,0 +1,1 @@
+Replace references to ``jasoncarver.eth`` with ``ens.eth``.
\ No newline at end of file
diff --git a/newsfragments/3025.feature.rst b/newsfragments/3025.feature.rst
new file mode 100644
index 00000000..bab751e1
--- /dev/null
+++ b/newsfragments/3025.feature.rst
@@ -0,0 +1,1 @@
+Add async support for the sign-and-send raw transaction middleware via ``construct_async_sign_and_send_raw_middleware()``.
diff --git a/web3/_utils/async_transactions.py b/web3/_utils/async_transactions.py
index 1f413b7e..f602d8e4 100644
--- a/web3/_utils/async_transactions.py
+++ b/web3/_utils/async_transactions.py
@@ -11,9 +11,10 @@ from eth_abi import (
 )
 from eth_typing import (
     URI,
+    ChecksumAddress,
 )
 from eth_utils.toolz import (
-    curry,
+    assoc,
     merge,
 )
 from hexbytes import (
@@ -78,7 +79,7 @@ TRANSACTION_DEFAULTS = {
     "value": 0,
     "data": b"",
     "gas": _estimate_gas,
-    "gasPrice": lambda w3, tx: w3.eth.generate_gas_price(tx),
+    "gasPrice": lambda async_w3, tx: async_w3.eth.generate_gas_price(tx),
     "maxFeePerGas": _max_fee_per_gas,
     "maxPriorityFeePerGas": _max_priority_fee_gas,
     "chainId": _chain_id,
@@ -113,12 +114,21 @@ async def get_buffered_gas_estimate(
     return min(gas_limit, gas_estimate + gas_buffer)
 
 
-@curry
-async def fill_transaction_defaults(
+async def async_fill_nonce(async_w3: "AsyncWeb3", transaction: TxParams) -> TxParams:
+    if "from" in transaction and "nonce" not in transaction:
+        tx_count = await async_w3.eth.get_transaction_count(
+            cast(ChecksumAddress, transaction["from"]),
+            block_identifier="pending",
+        )
+        return assoc(transaction, "nonce", tx_count)
+    return transaction
+
+
+async def async_fill_transaction_defaults(
     async_w3: "AsyncWeb3", transaction: TxParams
 ) -> TxParams:
     """
-    if w3 is None, fill as much as possible while offline
+    if async_w3 is None, fill as much as possible while offline
     """
     strategy_based_gas_price = async_w3.eth.generate_gas_price(transaction)
 
diff --git a/web3/contract/async_contract.py b/web3/contract/async_contract.py
index 5b907751..fd347930 100644
--- a/web3/contract/async_contract.py
+++ b/web3/contract/async_contract.py
@@ -32,7 +32,7 @@ from web3._utils.abi import (
     receive_func_abi_exists,
 )
 from web3._utils.async_transactions import (
-    fill_transaction_defaults as async_fill_transaction_defaults,
+    async_fill_transaction_defaults,
 )
 from web3._utils.compat import (
     Self,
diff --git a/web3/contract/utils.py b/web3/contract/utils.py
index 969f1384..f0dd8b14 100644
--- a/web3/contract/utils.py
+++ b/web3/contract/utils.py
@@ -29,7 +29,7 @@ from web3._utils.abi import (
     recursive_dict_to_namedtuple,
 )
 from web3._utils.async_transactions import (
-    fill_transaction_defaults as async_fill_transaction_defaults,
+    async_fill_transaction_defaults,
 )
 from web3._utils.contracts import (
     find_matching_fn_abi,
diff --git a/web3/middleware/signing.py b/web3/middleware/signing.py
index 04f59163..0ccc629f 100644
--- a/web3/middleware/signing.py
+++ b/web3/middleware/signing.py
@@ -27,6 +27,7 @@ from eth_typing import (
     HexStr,
 )
 from eth_utils import (
+    to_checksum_address,
     to_dict,
 )
 from eth_utils.curried import (
@@ -36,6 +37,10 @@ from eth_utils.toolz import (
     compose,
 )
 
+from web3._utils.async_transactions import (
+    async_fill_nonce,
+    async_fill_transaction_defaults,
+)
 from web3._utils.method_formatters import (
     STANDARD_NORMALIZERS,
 )
@@ -48,6 +53,8 @@ from web3._utils.transactions import (
     fill_transaction_defaults,
 )
 from web3.types import (
+    AsyncMiddleware,
+    AsyncMiddlewareCoroutine,
     Middleware,
     RPCEndpoint,
     RPCResponse,
@@ -55,7 +62,10 @@ from web3.types import (
 )
 
 if TYPE_CHECKING:
-    from web3 import Web3  # noqa: F401
+    from web3 import (  # noqa: F401
+        AsyncWeb3,
+        Web3,
+    )
 
 T = TypeVar("T")
 
@@ -168,8 +178,62 @@ def construct_sign_and_send_raw_middleware(
             account = accounts[transaction["from"]]
             raw_tx = account.sign_transaction(transaction).rawTransaction
 
-            return make_request(RPCEndpoint("eth_sendRawTransaction"), [raw_tx])
+            return make_request(RPCEndpoint("eth_sendRawTransaction"), [raw_tx.hex()])
 
         return middleware
 
     return sign_and_send_raw_middleware
+
+
+# -- async -- #
+
+
+async def async_construct_sign_and_send_raw_middleware(
+    private_key_or_account: Union[_PrivateKey, Collection[_PrivateKey]]
+) -> AsyncMiddleware:
+    """
+    Capture transactions & sign and send as raw transactions
+
+    Keyword arguments:
+    private_key_or_account -- A single private key or a tuple,
+    list or set of private keys. Keys can be any of the following formats:
+      - An eth_account.LocalAccount object
+      - An eth_keys.PrivateKey object
+      - A raw private key as a hex string or byte string
+    """
+    accounts = gen_normalized_accounts(private_key_or_account)
+
+    async def async_sign_and_send_raw_middleware(
+        make_request: Callable[[RPCEndpoint, Any], Any], async_w3: "AsyncWeb3"
+    ) -> AsyncMiddlewareCoroutine:
+        async def middleware(method: RPCEndpoint, params: Any) -> RPCResponse:
+            if method != "eth_sendTransaction":
+                # quick exit if not `eth_sendTransaction`
+                return await make_request(method, params)
+
+            formatted_transaction = format_transaction(params[0])
+            filled_transaction = await async_fill_transaction_defaults(
+                async_w3,
+                formatted_transaction,
+            )
+            filled_transaction = await async_fill_nonce(
+                async_w3,
+                filled_transaction,
+            )
+
+            tx_from = filled_transaction.get("from", None)
+
+            if tx_from is None or (tx_from is not None and tx_from not in accounts):
+                return await make_request(method, params)
+
+            account = accounts[to_checksum_address(tx_from)]
+            raw_tx = account.sign_transaction(filled_transaction).rawTransaction
+
+            return await make_request(
+                RPCEndpoint("eth_sendRawTransaction"),
+                [raw_tx.hex()],
+            )
+
+        return middleware
+
+    return async_sign_and_send_raw_middleware


Test patch:
diff --git a/tests/core/middleware/test_transaction_signing.py b/tests/core/middleware/test_transaction_signing.py
index 18384445..e1dc9e5a 100644
--- a/tests/core/middleware/test_transaction_signing.py
+++ b/tests/core/middleware/test_transaction_signing.py
@@ -12,6 +12,7 @@ from eth_tester.exceptions import (
 )
 from eth_utils import (
     ValidationError as EthUtilsValidationError,
+    is_hexstr,
     to_bytes,
     to_hex,
 )
@@ -25,24 +26,30 @@ from eth_utils.toolz import (
 from hexbytes import (
     HexBytes,
 )
+import pytest_asyncio
 
 from web3 import (
+    AsyncWeb3,
     Web3,
 )
 from web3.exceptions import (
     InvalidAddress,
 )
 from web3.middleware import (
+    async_construct_result_generator_middleware,
     construct_result_generator_middleware,
     construct_sign_and_send_raw_middleware,
 )
 from web3.middleware.signing import (
+    async_construct_sign_and_send_raw_middleware,
     gen_normalized_accounts,
 )
 from web3.providers import (
+    AsyncBaseProvider,
     BaseProvider,
 )
 from web3.providers.eth_tester import (
+    AsyncEthereumTesterProvider,
     EthereumTesterProvider,
 )
 
@@ -89,7 +96,7 @@ class DummyProvider(BaseProvider):
         raise NotImplementedError(f"Cannot make request for {method}:{params}")
 
 
-@pytest.fixture()
+@pytest.fixture
 def result_generator_middleware():
     return construct_result_generator_middleware(
         {
@@ -100,12 +107,12 @@ def result_generator_middleware():
     )
 
 
-@pytest.fixture()
+@pytest.fixture
 def w3_base():
     return Web3(provider=DummyProvider(), middlewares=[])
 
 
-@pytest.fixture()
+@pytest.fixture
 def w3_dummy(w3_base, result_generator_middleware):
     w3_base.middleware_onion.add(result_generator_middleware)
     return w3_base
@@ -115,90 +122,147 @@ def hex_to_bytes(s):
     return to_bytes(hexstr=s)
 
 
-@pytest.mark.parametrize(
-    "method,key_object,from_,expected",
+TEST_SIGN_AND_SEND_RAW_MIDDLEWARE_PARAMS = (
+    ("eth_sendTransaction", SAME_KEY_MIXED_TYPE, ADDRESS_2, NotImplementedError),
+    (
+        "eth_sendTransaction",
+        SAME_KEY_MIXED_TYPE,
+        ADDRESS_1,
+        "eth_sendRawTransaction",
+    ),
+    (
+        "eth_sendTransaction",
+        MIXED_KEY_MIXED_TYPE,
+        ADDRESS_2,
+        "eth_sendRawTransaction",
+    ),
+    (
+        "eth_sendTransaction",
+        MIXED_KEY_MIXED_TYPE,
+        ADDRESS_1,
+        "eth_sendRawTransaction",
+    ),
+    ("eth_sendTransaction", SAME_KEY_SAME_TYPE, ADDRESS_2, NotImplementedError),
+    (
+        "eth_sendTransaction",
+        SAME_KEY_SAME_TYPE,
+        ADDRESS_1,
+        "eth_sendRawTransaction",
+    ),
+    (
+        "eth_sendTransaction",
+        MIXED_KEY_SAME_TYPE,
+        ADDRESS_2,
+        "eth_sendRawTransaction",
+    ),
+    (
+        "eth_sendTransaction",
+        MIXED_KEY_SAME_TYPE,
+        ADDRESS_1,
+        "eth_sendRawTransaction",
+    ),
+    (
+        "eth_sendTransaction",
+        SAME_KEY_MIXED_TYPE[0],
+        ADDRESS_1,
+        "eth_sendRawTransaction",
+    ),
+    (
+        "eth_sendTransaction",
+        SAME_KEY_MIXED_TYPE[1],
+        ADDRESS_1,
+        "eth_sendRawTransaction",
+    ),
+    (
+        "eth_sendTransaction",
+        SAME_KEY_MIXED_TYPE[2],
+        ADDRESS_1,
+        "eth_sendRawTransaction",
+    ),
+    (
+        "eth_sendTransaction",
+        SAME_KEY_MIXED_TYPE[3],
+        ADDRESS_1,
+        "eth_sendRawTransaction",
+    ),
+    (
+        "eth_sendTransaction",
+        SAME_KEY_MIXED_TYPE[4],
+        ADDRESS_1,
+        "eth_sendRawTransaction",
+    ),
+    ("eth_sendTransaction", SAME_KEY_MIXED_TYPE[0], ADDRESS_2, NotImplementedError),
+    ("eth_sendTransaction", SAME_KEY_MIXED_TYPE[1], ADDRESS_2, NotImplementedError),
+    ("eth_sendTransaction", SAME_KEY_MIXED_TYPE[2], ADDRESS_2, NotImplementedError),
+    ("eth_sendTransaction", SAME_KEY_MIXED_TYPE[3], ADDRESS_2, NotImplementedError),
+    ("eth_sendTransaction", SAME_KEY_MIXED_TYPE[4], ADDRESS_2, NotImplementedError),
+    ("eth_call", MIXED_KEY_MIXED_TYPE, ADDRESS_1, NotImplementedError),
+    (
+        "eth_sendTransaction",
+        SAME_KEY_SAME_TYPE,
+        hex_to_bytes(ADDRESS_1),
+        "eth_sendRawTransaction",
+    ),
+)
+TEST_SIGNED_TRANSACTION_PARAMS = (
+    (
+        {"gas": 21000, "gasPrice": 10**9, "value": 1},
+        -1,
+        MIXED_KEY_MIXED_TYPE,
+        ADDRESS_1,
+    ),
+    (
+        {"value": 1},
+        -1,
+        MIXED_KEY_MIXED_TYPE,
+        ADDRESS_1,
+    ),
+    # expect validation error + unmanaged account
+    (
+        {"gas": 21000, "value": 10},
+        ValidationError,
+        SAME_KEY_MIXED_TYPE,
+        ADDRESS_2,
+    ),
     (
-        ("eth_sendTransaction", SAME_KEY_MIXED_TYPE, ADDRESS_2, NotImplementedError),
-        (
-            "eth_sendTransaction",
-            SAME_KEY_MIXED_TYPE,
-            ADDRESS_1,
-            "eth_sendRawTransaction",
-        ),
-        (
-            "eth_sendTransaction",
-            MIXED_KEY_MIXED_TYPE,
-            ADDRESS_2,
-            "eth_sendRawTransaction",
-        ),
-        (
-            "eth_sendTransaction",
-            MIXED_KEY_MIXED_TYPE,
-            ADDRESS_1,
-            "eth_sendRawTransaction",
-        ),
-        ("eth_sendTransaction", SAME_KEY_SAME_TYPE, ADDRESS_2, NotImplementedError),
-        (
-            "eth_sendTransaction",
-            SAME_KEY_SAME_TYPE,
-            ADDRESS_1,
-            "eth_sendRawTransaction",
-        ),
-        (
-            "eth_sendTransaction",
-            MIXED_KEY_SAME_TYPE,
-            ADDRESS_2,
-            "eth_sendRawTransaction",
-        ),
-        (
-            "eth_sendTransaction",
-            MIXED_KEY_SAME_TYPE,
-            ADDRESS_1,
-            "eth_sendRawTransaction",
-        ),
-        (
-            "eth_sendTransaction",
-            SAME_KEY_MIXED_TYPE[0],
-            ADDRESS_1,
-            "eth_sendRawTransaction",
-        ),
-        (
-            "eth_sendTransaction",
-            SAME_KEY_MIXED_TYPE[1],
-            ADDRESS_1,
-            "eth_sendRawTransaction",
-        ),
-        (
-            "eth_sendTransaction",
-            SAME_KEY_MIXED_TYPE[2],
-            ADDRESS_1,
-            "eth_sendRawTransaction",
-        ),
-        (
-            "eth_sendTransaction",
-            SAME_KEY_MIXED_TYPE[3],
-            ADDRESS_1,
-            "eth_sendRawTransaction",
-        ),
-        (
-            "eth_sendTransaction",
-            SAME_KEY_MIXED_TYPE[4],
-            ADDRESS_1,
-            "eth_sendRawTransaction",
-        ),
-        ("eth_sendTransaction", SAME_KEY_MIXED_TYPE[0], ADDRESS_2, NotImplementedError),
-        ("eth_sendTransaction", SAME_KEY_MIXED_TYPE[1], ADDRESS_2, NotImplementedError),
-        ("eth_sendTransaction", SAME_KEY_MIXED_TYPE[2], ADDRESS_2, NotImplementedError),
-        ("eth_sendTransaction", SAME_KEY_MIXED_TYPE[3], ADDRESS_2, NotImplementedError),
-        ("eth_sendTransaction", SAME_KEY_MIXED_TYPE[4], ADDRESS_2, NotImplementedError),
-        ("eth_call", MIXED_KEY_MIXED_TYPE, ADDRESS_1, NotImplementedError),
-        (
-            "eth_sendTransaction",
-            SAME_KEY_SAME_TYPE,
-            hex_to_bytes(ADDRESS_1),
-            "eth_sendRawTransaction",
-        ),
+        {"gas": 21000, "value": 10},
+        InvalidAddress,
+        SAME_KEY_MIXED_TYPE,
+        "0x0000",
     ),
+    (
+        {"gas": 21000, "gasPrice": 0, "value": 1},
+        EthUtilsValidationError,
+        MIXED_KEY_MIXED_TYPE,
+        ADDRESS_1,
+    ),
+    (
+        {
+            "type": "0x2",
+            "value": 22,
+            "maxFeePerGas": 2000000000,
+            "maxPriorityFeePerGas": 10**9,
+        },
+        -1,
+        SAME_KEY_MIXED_TYPE,
+        ADDRESS_1,
+    ),
+    (
+        {
+            "value": 22,
+            "maxFeePerGas": 20**9,
+            "maxPriorityFeePerGas": 10**9,
+        },
+        -1,
+        SAME_KEY_MIXED_TYPE,
+        ADDRESS_1,
+    ),
+)
+
+
+@pytest.mark.parametrize(
+    "method,key_object,from_,expected",
+    TEST_SIGN_AND_SEND_RAW_MIDDLEWARE_PARAMS,
 )
 def test_sign_and_send_raw_middleware(w3_dummy, method, from_, expected, key_object):
     w3_dummy.middleware_onion.add(construct_sign_and_send_raw_middleware(key_object))
@@ -247,7 +311,7 @@ def assert_method_and_txn_signed(actual, expected):
     raw_txn = actual[1][0]
     actual_method = actual[0]
     assert actual_method == expected
-    assert isinstance(raw_txn, bytes)
+    assert is_hexstr(raw_txn)
 
 
 @pytest.fixture()
@@ -279,7 +343,7 @@ def test_gen_normalized_accounts_type_error(w3):
         gen_normalized_accounts(1234567890)
 
 
-@pytest.fixture()
+@pytest.fixture
 def fund_account(w3):
     # fund local account
     tx_value = w3.to_wei(10, "ether")
@@ -292,60 +356,7 @@ def fund_account(w3):
 
 @pytest.mark.parametrize(
     "transaction,expected,key_object,from_",
-    (
-        (
-            {"gas": 21000, "gasPrice": 10**9, "value": 1},
-            -1,
-            MIXED_KEY_MIXED_TYPE,
-            ADDRESS_1,
-        ),
-        (
-            {"value": 1},
-            -1,
-            MIXED_KEY_MIXED_TYPE,
-            ADDRESS_1,
-        ),
-        # expect validation error + unmanaged account
-        (
-            {"gas": 21000, "value": 10},
-            ValidationError,
-            SAME_KEY_MIXED_TYPE,
-            ADDRESS_2,
-        ),
-        (
-            {"gas": 21000, "value": 10},
-            InvalidAddress,
-            SAME_KEY_MIXED_TYPE,
-            "0x0000",
-        ),
-        (
-            {"gas": 21000, "gasPrice": 0, "value": 1},
-            EthUtilsValidationError,
-            MIXED_KEY_MIXED_TYPE,
-            ADDRESS_1,
-        ),
-        (
-            {
-                "type": "0x2",
-                "value": 22,
-                "maxFeePerGas": 2000000000,
-                "maxPriorityFeePerGas": 10**9,
-            },
-            -1,
-            SAME_KEY_MIXED_TYPE,
-            ADDRESS_1,
-        ),
-        (
-            {
-                "value": 22,
-                "maxFeePerGas": 20**9,
-                "maxPriorityFeePerGas": 10**9,
-            },
-            -1,
-            SAME_KEY_MIXED_TYPE,
-            ADDRESS_1,
-        ),
-    ),
+    TEST_SIGNED_TRANSACTION_PARAMS,
     ids=[
         "with set gas",
         "with no set gas",
@@ -407,4 +418,200 @@ def test_sign_and_send_raw_middleware_with_byte_addresses(
     raw_txn = actual[1][0]
     actual_method = actual[0]
     assert actual_method == "eth_sendRawTransaction"
-    assert isinstance(raw_txn, bytes)
+    assert is_hexstr(raw_txn)
+
+
+# -- async -- #
+
+
+@pytest_asyncio.fixture
+async def async_result_generator_middleware():
+    return await async_construct_result_generator_middleware(
+        {
+            "eth_sendRawTransaction": lambda *args: args,
+            "net_version": lambda *_: 1,
+            "eth_chainId": lambda *_: "0x02",
+        }
+    )
+
+
+class AsyncDummyProvider(AsyncBaseProvider):
+    async def coro_request(self, method, params):
+        raise NotImplementedError(f"Cannot make request for {method}:{params}")
+
+
+@pytest.fixture
+def async_w3_base():
+    return AsyncWeb3(provider=AsyncDummyProvider(), middlewares=[])
+
+
+@pytest.fixture
+def async_w3_dummy(async_w3_base, async_result_generator_middleware):
+    async_w3_base.middleware_onion.add(async_result_generator_middleware)
+    return async_w3_base
+
+
+@pytest.fixture
+def async_w3():
+    return AsyncWeb3(AsyncEthereumTesterProvider())
+
+
+@pytest_asyncio.fixture
+async def async_fund_account(async_w3):
+    # fund local account
+    accounts = await async_w3.eth.accounts
+    tx_value = async_w3.to_wei(10, "ether")
+    for address in (ADDRESS_1, ADDRESS_2):
+        await async_w3.eth.send_transaction(
+            {"to": address, "from": accounts[0], "gas": 21000, "value": tx_value}
+        )
+        acct_bal = await async_w3.eth.get_balance(address)
+        assert acct_bal == tx_value
+
+
+@pytest.mark.parametrize(
+    "method,key_object,from_,expected",
+    TEST_SIGN_AND_SEND_RAW_MIDDLEWARE_PARAMS,
+)
+@pytest.mark.asyncio
+async def test_async_sign_and_send_raw_middleware(
+    async_w3_dummy,
+    method,
+    from_,
+    expected,
+    key_object,
+):
+    async_w3_dummy.middleware_onion.add(
+        await async_construct_sign_and_send_raw_middleware(key_object)
+    )
+
+    legacy_transaction = {
+        "to": "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf",
+        "from": from_,
+        "gas": 21000,
+        "gasPrice": 10**9,
+        "value": 1,
+        "nonce": 0,
+    }
+    if isinstance(expected, type) and issubclass(expected, Exception):
+        with pytest.raises(expected):
+            await async_w3_dummy.manager.coro_request(
+                method,
+                [legacy_transaction],
+            )
+    else:
+        # assert with legacy txn params
+        actual = await async_w3_dummy.manager.coro_request(
+            method,
+            [legacy_transaction],
+        )
+        assert_method_and_txn_signed(actual, expected)
+
+        # assert with dynamic fee transaction params and explicit type
+        dynamic_fee_transaction = dissoc(legacy_transaction, "gasPrice")
+        dynamic_fee_transaction = assoc(
+            dynamic_fee_transaction, "maxFeePerGas", 2000000000
+        )
+        dynamic_fee_transaction = assoc(
+            dynamic_fee_transaction, "maxPriorityFeePerGas", 1000000000
+        )
+        dynamic_fee_transaction = assoc(dynamic_fee_transaction, "type", "0x2")
+
+        actual_dynamic_fee_call = await async_w3_dummy.manager.coro_request(
+            method, [dynamic_fee_transaction]
+        )
+        assert_method_and_txn_signed(actual_dynamic_fee_call, expected)
+
+        # assert with dynamic fee transaction params and no explicit type
+        dynamic_fee_transaction_no_type = dissoc(dynamic_fee_transaction, "type")
+
+        actual_dynamic_fee_call_no_type = await async_w3_dummy.manager.coro_request(
+            method, [dynamic_fee_transaction_no_type]
+        )
+        assert_method_and_txn_signed(actual_dynamic_fee_call_no_type, expected)
+
+
+@pytest.mark.parametrize(
+    "transaction,expected,key_object,from_",
+    TEST_SIGNED_TRANSACTION_PARAMS,
+    ids=[
+        "with set gas",
+        "with no set gas",
+        "with mismatched sender",
+        "with invalid sender",
+        "with gasPrice lower than base fee",
+        "with txn type and dynamic fee txn params",
+        "with dynamic fee txn params and no type",
+    ],
+)
+@pytest.mark.asyncio
+async def test_async_signed_transaction(
+    async_w3,
+    async_fund_account,
+    transaction,
+    expected,
+    key_object,
+    from_,
+):
+    async_w3.middleware_onion.add(
+        await async_construct_sign_and_send_raw_middleware(key_object)
+    )
+
+    # Drop any falsy addresses
+    accounts = await async_w3.eth.accounts
+    to_from = valfilter(bool, {"to": accounts[0], "from": from_})
+
+    _transaction = merge(transaction, to_from)
+
+    if isinstance(expected, type) and issubclass(expected, Exception):
+        with pytest.raises(expected):
+            await async_w3.eth.send_transaction(_transaction)
+    else:
+        start_balance = await async_w3.eth.get_balance(
+            _transaction.get("from", accounts[0])
+        )
+        await async_w3.eth.send_transaction(_transaction)
+        assert (
+            await async_w3.eth.get_balance(_transaction.get("from"))
+            <= start_balance + expected
+        )
+
+
+@pytest.mark.parametrize(
+    "from_converter,to_converter",
+    (
+        (identity, identity),
+        (hex_to_bytes, identity),
+        (identity, hex_to_bytes),
+        (hex_to_bytes, hex_to_bytes),
+    ),
+)
+@pytest.mark.asyncio
+async def test_async_sign_and_send_raw_middleware_with_byte_addresses(
+    async_w3_dummy, from_converter, to_converter
+):
+    private_key = PRIVATE_KEY_1
+    from_ = from_converter(ADDRESS_1)
+    to_ = to_converter(ADDRESS_2)
+
+    async_w3_dummy.middleware_onion.add(
+        await async_construct_sign_and_send_raw_middleware(private_key)
+    )
+
+    actual = await async_w3_dummy.manager.coro_request(
+        "eth_sendTransaction",
+        [
+            {
+                "to": to_,
+                "from": from_,
+                "gas": 21000,
+                "gasPrice": 0,
+                "value": 1,
+                "nonce": 0,
+            }
+        ],
+    )
+    raw_txn = actual[1][0]
+    actual_method = actual[0]
+    assert actual_method == "eth_sendRawTransaction"
+    assert is_hexstr(raw_txn)
diff --git a/tests/core/utilities/test_async_transaction.py b/tests/core/utilities/test_async_transaction.py
index a3edee9a..e7ca7678 100644
--- a/tests/core/utilities/test_async_transaction.py
+++ b/tests/core/utilities/test_async_transaction.py
@@ -5,7 +5,7 @@ from eth_typing import (
 )
 
 from web3._utils.async_transactions import (
-    fill_transaction_defaults,
+    async_fill_transaction_defaults,
     get_block_gas_limit,
     get_buffered_gas_estimate,
 )
@@ -52,7 +52,7 @@ async def test_get_buffered_gas_estimate(async_w3):
 
 @pytest.mark.asyncio()
 async def test_fill_transaction_defaults_for_all_params(async_w3):
-    default_transaction = await fill_transaction_defaults(async_w3, {})
+    default_transaction = await async_fill_transaction_defaults(async_w3, {})
 
     block = await async_w3.eth.get_block("latest")
     assert default_transaction == {
@@ -72,7 +72,7 @@ async def test_fill_transaction_defaults_nondynamic_tranaction_fee(async_w3):
     gasPrice_transaction = {
         "gasPrice": 10,
     }
-    default_transaction = await fill_transaction_defaults(
+    default_transaction = await async_fill_transaction_defaults(
         async_w3, gasPrice_transaction
     )
 
@@ -86,7 +86,7 @@ async def test_fill_transaction_defaults_for_zero_gas_price(async_w3):
 
     async_w3.eth.set_gas_price_strategy(gas_price_strategy)
 
-    default_transaction = await fill_transaction_defaults(async_w3, {})
+    default_transaction = await async_fill_transaction_defaults(async_w3, {})
 
     assert default_transaction == {
         "chainId": await async_w3.eth.chain_id,