Repository: ethereum/web3.py

Issue:
ENS.fromWeb3 not copying middleware
* Version: 5.10.0
* Python: 3.8
* OS: linux

### What was wrong?

I would expect that after configuring a Web3 instance, if I do `ENS.fromWeb3(w3)` it would copy the middleware and parameters needed. Instead, it just uses the provider. This is very important for using ENS with Rinkeby and Geth, as it needs the POA middleware.

Steps to reproduce:

Initialize instance of Web3, add middleware for POA and then create an ENS instance from that.

### How can it be fixed?

Copy web3 instance or allow to provide one.


Discussion:
Thanks for the report. Seems like a reasonable expectation. 

I see that `fromWeb3` just [peels the provider](https://github.com/ethereum/web3.py/blob/master/ens/main.py#L102) off of the web3 instance passed in, because ENS accepts a provider. Upon creation, there's also some [adding and removing of middleware](https://github.com/ethereum/web3.py/blob/master/ens/utils.py#L91-L95) as a result of the `init_web3` method.

@pipermerriam @carver thoughts on passing/cloning a web3 instance into ENS to preserve the middleware onion? Was this avoided in the past for any reason? If you like the idea, do you have a preferred implementation?
Yeah, there are definitely some quirks there. But I *think* they're all managed by the methods @marcgarreau  already found at https://github.com/ethereum/web3.py/blob/6258d2e1c807aee892481296c7f115399ce6a6f8/ens/utils.py#L77-L96

So the bulk of the work is to pass in a "template" w3 instance and copy the existing middlewares & provider, as a kind of alternative to `init_web3()`, then apply `customize_web3()`

(Also a reminder that @Uxio0 will have to pass `addr` into `ENS.fromWeb3()` for non-mainnet usage, since ENS is hosted at a different addresses on different networks IIRC)

Resolving patch:
diff --git a/ens/main.py b/ens/main.py
--- a/ens/main.py
+++ b/ens/main.py
@@ -58,6 +58,7 @@
         BaseProvider,
     )
     from web3.types import (  # noqa: F401
+        Middleware,
         TxParams,
     )
 
@@ -98,7 +99,10 @@ def reverse_domain(address: ChecksumAddress) -> str:
         return address_to_reverse_domain(address)
 
     def __init__(
-        self, provider: 'BaseProvider' = cast('BaseProvider', default), addr: ChecksumAddress = None
+        self,
+        provider: 'BaseProvider' = cast('BaseProvider', default),
+        middlewares: Optional[Sequence[Tuple['Middleware', str]]] = None,
+        addr: ChecksumAddress = None,
     ) -> None:
         """
         :param provider: a single provider used to connect to Ethereum
@@ -106,7 +110,7 @@ def __init__(
         :param hex-string addr: the address of the ENS registry on-chain. If not provided,
             ENS.py will default to the mainnet ENS registry address.
         """
-        self.web3 = init_web3(provider)
+        self.web3 = init_web3(provider, middlewares)
 
         ens_addr = addr if addr else ENS_MAINNET_ADDR
         self.ens = self.web3.eth.contract(abi=abis.ENS, address=ens_addr)
@@ -121,7 +125,9 @@ def fromWeb3(cls, web3: 'Web3', addr: ChecksumAddress = None) -> 'ENS':
         :param hex-string addr: the address of the ENS registry on-chain. If not provided,
             ENS.py will default to the mainnet ENS registry address.
         """
-        return cls(web3.manager.provider, addr=addr)
+        provider = web3.manager.provider
+        middlewares = web3.middleware_onion.middlewares
+        return cls(provider, middlewares, addr=addr)
 
     def address(self, name: str) -> Optional[ChecksumAddress]:
         """
diff --git a/ens/utils.py b/ens/utils.py
--- a/ens/utils.py
+++ b/ens/utils.py
@@ -7,6 +7,8 @@
     Callable,
     Collection,
     Optional,
+    Sequence,
+    Tuple,
     Type,
     TypeVar,
     Union,
@@ -49,6 +51,9 @@
     from web3.providers import (  # noqa: F401
         BaseProvider,
     )
+    from web3.types import (  # noqa: F401
+        Middleware,
+    )
 
 
 def Web3() -> Type['_Web3']:
@@ -68,13 +73,16 @@ def wrapper(*args: Any, **kwargs: Any) -> TFunc:
     return cast(TFunc, wrapper)
 
 
-def init_web3(provider: 'BaseProvider' = cast('BaseProvider', default)) -> '_Web3':
+def init_web3(
+    provider: 'BaseProvider' = cast('BaseProvider', default),
+    middlewares: Optional[Sequence[Tuple['Middleware', str]]] = None,
+) -> '_Web3':
     from web3 import Web3 as Web3Main
 
     if provider is default:
         w3 = Web3Main(ens=None)
     else:
-        w3 = Web3Main(provider, ens=None)
+        w3 = Web3Main(provider, middlewares, ens=None)
 
     return customize_web3(w3)
 
@@ -82,11 +90,15 @@ def init_web3(provider: 'BaseProvider' = cast('BaseProvider', default)) -> '_Web
 def customize_web3(w3: '_Web3') -> '_Web3':
     from web3.middleware import make_stalecheck_middleware
 
-    w3.middleware_onion.remove('name_to_address')
-    w3.middleware_onion.add(
-        make_stalecheck_middleware(ACCEPTABLE_STALE_HOURS * 3600),
-        name='stalecheck',
-    )
+    if w3.middleware_onion.get('name_to_address'):
+        w3.middleware_onion.remove('name_to_address')
+
+    if not w3.middleware_onion.get('stalecheck'):
+        w3.middleware_onion.add(
+            make_stalecheck_middleware(ACCEPTABLE_STALE_HOURS * 3600),
+            name='stalecheck'
+        )
+
     return w3
 
 
diff --git a/web3/datastructures.py b/web3/datastructures.py
--- a/web3/datastructures.py
+++ b/web3/datastructures.py
@@ -196,6 +196,14 @@ def remove(self, old: TKey) -> None:
             raise ValueError("You can only remove something that has been added")
         del self._queue[old]
 
+    @property
+    def middlewares(self) -> Sequence[Any]:
+        """
+        Returns middlewares in the appropriate order to be imported into a new Web3 instance
+        (reversed _queue order) as a list of (middleware, name) tuples.
+        """
+        return [(val, key) for key, val in reversed(self._queue.items())]
+
     def _replace_with_new_name(self, old: TKey, new: TKey) -> None:
         self._queue[new] = new
         found_old = False


Test patch:
diff --git a/tests/core/manager/test_middleware_add_and_clear_api.py b/tests/core/manager/test_middleware_onion_api.py
similarity index 91%
rename from tests/core/manager/test_middleware_add_and_clear_api.py
rename to tests/core/manager/test_middleware_onion_api.py
--- a/tests/core/manager/test_middleware_add_and_clear_api.py
+++ b/tests/core/manager/test_middleware_onion_api.py
@@ -66,7 +66,7 @@ def test_add_named_duplicate_middleware(middleware_factory):
 def test_add_duplicate_middleware(middleware_factory):
     mw = middleware_factory()
     with pytest.raises(ValueError):
-        manager = RequestManager(None, BaseProvider(), middlewares=[mw, mw])
+        RequestManager(None, BaseProvider(), middlewares=[mw, mw])
 
     manager = RequestManager(None, BaseProvider(), middlewares=[])
     manager.middleware_onion.add(mw)
@@ -168,3 +168,13 @@ def test_remove_middleware(middleware_factory):
     manager.middleware_onion.remove(mw2)
 
     assert tuple(manager.middleware_onion) == (mw1, mw3)
+
+
+def test_export_middlewares(middleware_factory):
+    mw1 = middleware_factory()
+    mw2 = middleware_factory()
+    manager = RequestManager(None, BaseProvider(), middlewares=[(mw1, 'name1'), (mw2, 'name2')])
+    assert tuple(manager.middleware_onion) == (mw1, mw2)
+
+    middlewares = manager.middleware_onion.middlewares
+    assert middlewares == [(mw1, 'name1'), (mw2, 'name2')]
diff --git a/tests/ens/test_ens.py b/tests/ens/test_ens.py
new file mode 100644
--- /dev/null
+++ b/tests/ens/test_ens.py
@@ -0,0 +1,12 @@
+from ens import ENS
+from web3.middleware import (
+    pythonic_middleware,
+)
+
+
+def test_fromWeb3_inherits_web3_middlewares(web3):
+    test_middleware = pythonic_middleware
+    web3.middleware_onion.add(test_middleware, 'test_middleware')
+
+    ns = ENS.fromWeb3(web3)
+    assert ns.web3.middleware_onion.get('test_middleware') == test_middleware