Repository: ethereum/web3.py

Issue:
[WIP] Add eth.get_raw_transaction_by_block #2040
### What was wrong?
RPC Methods ```eth_getRawTransactionByBlockHashAndIndex```, and ```eth_getRawTransactionByBlockNumberAndIndex``` are not supported. This PR adds support for it and also add eth.get_raw_transaction_by_block method to web3/eth.py

Related to Issue #2040 

### How was it fixed?
Implementation closely followed to that of ```eth.get_transaction_by_block```

### Todo:
- [X] Add ```eth.get_raw_transaction_by_block``` method in ```web3/eth.py``` 
- [x] Add ```eth_getRawTransactionByBlockHashAndIndex```, and ```eth_getRawTransactionByBlockNumberAndIndex``` methods in ```web3/_utils/rpc_abi.py```
- [x] Add ```eth_getRawTransactionByBlockHashAndIndex```, and ```eth_getRawTransactionByBlockNumberAndIndex``` method formatting and error handling in ```web3/_utils/method_formatters.py```
- [ ] Add tests


#### Cute Animal Picture

![Put a link to a cute animal picture inside the parenthesis-->](https://i.pinimg.com/originals/b3/67/03/b367039be15d14f61363bb59b3752ed2.jpg)



Discussion:

Resolving patch:
diff --git a/web3/_utils/method_formatters.py b/web3/_utils/method_formatters.py
--- a/web3/_utils/method_formatters.py
+++ b/web3/_utils/method_formatters.py
@@ -409,6 +409,11 @@ def apply_list_to_array_formatter(formatter: Any) -> Callable[..., Any]:
         apply_formatter_at_index(to_hex_if_integer, 1),
     ),
     RPC.eth_getTransactionCount: apply_formatter_at_index(to_hex_if_integer, 1),
+    RPC.eth_getRawTransactionByBlockNumberAndIndex: compose(
+        apply_formatter_at_index(to_hex_if_integer, 0),
+        apply_formatter_at_index(to_hex_if_integer, 1),
+    ),
+    RPC.eth_getRawTransactionByBlockHashAndIndex: apply_formatter_at_index(to_hex_if_integer, 1),
     RPC.eth_getUncleCountByBlockNumber: apply_formatter_at_index(to_hex_if_integer, 0),
     RPC.eth_getUncleByBlockNumberAndIndex: compose(
         apply_formatter_at_index(to_hex_if_integer, 0),
@@ -468,6 +473,8 @@ def apply_list_to_array_formatter(formatter: Any) -> Callable[..., Any]:
     RPC.eth_getFilterLogs: filter_result_formatter,
     RPC.eth_getLogs: filter_result_formatter,
     RPC.eth_getProof: apply_formatter_if(is_not_null, proof_formatter),
+    RPC.eth_getRawTransactionByBlockHashAndIndex: HexBytes,
+    RPC.eth_getRawTransactionByBlockNumberAndIndex: HexBytes,
     RPC.eth_getRawTransactionByHash: HexBytes,
     RPC.eth_getStorageAt: HexBytes,
     RPC.eth_getTransactionByBlockHashAndIndex: apply_formatter_if(
@@ -676,6 +683,8 @@ def raise_transaction_not_found_with_index(params: Tuple[BlockIdentifier, int])
     RPC.eth_getTransactionByBlockHashAndIndex: raise_transaction_not_found_with_index,
     RPC.eth_getTransactionByBlockNumberAndIndex: raise_transaction_not_found_with_index,
     RPC.eth_getTransactionReceipt: raise_transaction_not_found,
+    RPC.eth_getRawTransactionByBlockHashAndIndex: raise_transaction_not_found_with_index,
+    RPC.eth_getRawTransactionByBlockNumberAndIndex: raise_transaction_not_found_with_index,
     RPC.eth_getRawTransactionByHash: raise_transaction_not_found,
 }
 
diff --git a/web3/_utils/rpc_abi.py b/web3/_utils/rpc_abi.py
--- a/web3/_utils/rpc_abi.py
+++ b/web3/_utils/rpc_abi.py
@@ -63,6 +63,12 @@ class RPC:
     eth_getStorageAt = RPCEndpoint("eth_getStorageAt")
     eth_getTransactionByBlockHashAndIndex = RPCEndpoint("eth_getTransactionByBlockHashAndIndex")
     eth_getTransactionByBlockNumberAndIndex = RPCEndpoint("eth_getTransactionByBlockNumberAndIndex")
+    eth_getRawTransactionByBlockHashAndIndex = RPCEndpoint(
+        "eth_getRawTransactionByBlockHashAndIndex"
+    )
+    eth_getRawTransactionByBlockNumberAndIndex = RPCEndpoint(
+        "eth_getRawTransactionByBlockNumberAndIndex"
+    )
     eth_getTransactionByHash = RPCEndpoint("eth_getTransactionByHash")
     eth_getTransactionCount = RPCEndpoint("eth_getTransactionCount")
     eth_getTransactionReceipt = RPCEndpoint("eth_getTransactionReceipt")
@@ -185,6 +191,7 @@ class RPC:
     'eth_getTransactionByHash': ['bytes32'],
     'eth_getTransactionCount': ['address', None],
     'eth_getTransactionReceipt': ['bytes32'],
+    'eth_getRawTransactionByBlockHashAndIndex': ['bytes32', 'uint'],
     'eth_getUncleCountByBlockHash': ['bytes32'],
     'eth_newFilter': FILTER_PARAMS_ABIS,
     'eth_sendRawTransaction': ['bytes'],
diff --git a/web3/eth.py b/web3/eth.py
--- a/web3/eth.py
+++ b/web3/eth.py
@@ -155,6 +155,19 @@ def send_transaction_munger(self, transaction: TxParams) -> Tuple[TxParams]:
         mungers=[default_root_munger]
     )
 
+    """
+    `eth_getRawTransactionByBlockHashAndIndex`
+    `eth_getRawTransactionByBlockNumberAndIndex`
+    """
+    _get_raw_transaction_by_block: Method[Callable[[BlockIdentifier, int], HexBytes]] = Method(
+        method_choice_depends_on_args=select_method_for_block_identifier(
+            if_predefined=RPC.eth_getRawTransactionByBlockNumberAndIndex,
+            if_hash=RPC.eth_getRawTransactionByBlockHashAndIndex,
+            if_number=RPC.eth_getRawTransactionByBlockNumberAndIndex,
+        ),
+        mungers=[default_root_munger]
+    )
+
     def _generate_gas_price(self, transaction_params: Optional[TxParams] = None) -> Optional[Wei]:
         if self.gasPriceStrategy:
             return self.gasPriceStrategy(self.web3, transaction_params)
@@ -287,6 +300,12 @@ async def get_raw_transaction(self, transaction_hash: _Hash32) -> TxData:
         # types ignored b/c mypy conflict with BlockingEth properties
         return await self._get_raw_transaction(transaction_hash)  # type: ignore
 
+    async def get_raw_transaction_by_block(
+        self, block_identifier: BlockIdentifier, index: int
+    ) -> HexBytes:
+        # types ignored b/c mypy conflict with BlockingEth properties
+        return await self._get_raw_transaction_by_block(block_identifier, index)  # type: ignore
+
     async def generate_gas_price(
         self, transaction_params: Optional[TxParams] = None
     ) -> Optional[Wei]:
@@ -619,6 +638,11 @@ def get_transaction(self, transaction_hash: _Hash32) -> TxData:
     def get_raw_transaction(self, transaction_hash: _Hash32) -> _Hash32:
         return self._get_raw_transaction(transaction_hash)
 
+    def get_raw_transaction_by_block(
+        self, block_identifier: BlockIdentifier, index: int
+    ) -> HexBytes:
+        return self._get_raw_transaction_by_block(block_identifier, index)
+
     def getTransactionFromBlock(
         self, block_identifier: BlockIdentifier, transaction_index: int
     ) -> NoReturn:


Test patch:
diff --git a/tests/integration/test_ethereum_tester.py b/tests/integration/test_ethereum_tester.py
--- a/tests/integration/test_ethereum_tester.py
+++ b/tests/integration/test_ethereum_tester.py
@@ -276,6 +276,12 @@ class TestEthereumTesterEthModule(EthModuleTest):
         EthModuleTest.test_eth_get_raw_transaction, ValueError)
     test_eth_get_raw_transaction_raises_error = not_implemented(
         EthModuleTest.test_eth_get_raw_transaction, ValueError)
+    test_eth_get_raw_transaction_by_block = not_implemented(
+        EthModuleTest.test_eth_get_raw_transaction_by_block, ValueError
+    )
+    test_eth_get_raw_transaction_by_block_raises_error = not_implemented(
+        EthModuleTest.test_eth_get_raw_transaction_by_block, ValueError
+    )
     test_eth_replace_transaction_already_mined = not_implemented(
         EthModuleTest.test_eth_replace_transaction_already_mined, ValueError
     )
diff --git a/web3/_utils/module_testing/eth_module.py b/web3/_utils/module_testing/eth_module.py
--- a/web3/_utils/module_testing/eth_module.py
+++ b/web3/_utils/module_testing/eth_module.py
@@ -8,6 +8,7 @@
     TYPE_CHECKING,
     Callable,
     Sequence,
+    Union,
     cast,
 )
 
@@ -37,6 +38,9 @@
 from web3._utils.ens import (
     ens_addresses,
 )
+from web3._utils.method_formatters import (
+    to_hex_if_integer,
+)
 from web3.exceptions import (
     BlockNotFound,
     ContractLogicError,
@@ -473,11 +477,71 @@ async def test_eth_get_raw_transaction(
         assert is_bytes(raw_transaction)
 
     @pytest.mark.asyncio
-    async def test_eth_get_raw_transaction_raises_error(
-        self, web3: "Web3", mined_txn_hash: HexStr
-    ) -> None:
+    async def test_eth_get_raw_transaction_raises_error(self, async_w3: "Web3") -> None:
         with pytest.raises(TransactionNotFound, match=f"Transaction with hash: '{UNKNOWN_HASH}'"):
-            web3.eth.get_raw_transaction(UNKNOWN_HASH)
+            await async_w3.eth.get_raw_transaction(UNKNOWN_HASH)  # type: ignore
+
+    @pytest.mark.asyncio
+    async def test_eth_get_raw_transaction_by_block(
+        self,
+        async_w3: "Web3",
+        block_with_txn: BlockData,
+        unlocked_account_dual_type: ChecksumAddress
+    ) -> None:
+        # eth_getRawTransactionByBlockNumberAndIndex: block identifier
+        # send a txn to make sure pending block has at least one txn
+        await async_w3.eth.send_transaction(  # type: ignore
+            {
+                'from': unlocked_account_dual_type,
+                'to': unlocked_account_dual_type,
+                'value': Wei(1),
+            }
+        )
+        pending_block = await async_w3.eth.get_block('pending')  # type: ignore
+        last_pending_txn_index = len(pending_block['transactions']) - 1
+        raw_txn = await async_w3.eth.get_raw_transaction_by_block(  # type: ignore
+            'pending', last_pending_txn_index
+        )
+        assert is_bytes(raw_txn)
+
+        # eth_getRawTransactionByBlockNumberAndIndex: block number
+        block_with_txn_number = block_with_txn['number']
+        raw_transaction = await async_w3.eth.get_raw_transaction_by_block(  # type: ignore
+            block_with_txn_number, 0
+        )
+        assert is_bytes(raw_transaction)
+
+        # eth_getRawTransactionByBlockHashAndIndex: block hash
+        block_with_txn_hash = block_with_txn['hash']
+        raw_transaction = await async_w3.eth.get_raw_transaction_by_block(  # type: ignore
+            block_with_txn_hash, 0
+        )
+        assert is_bytes(raw_transaction)
+
+    @pytest.mark.asyncio
+    @pytest.mark.parametrize('unknown_block_num_or_hash', (1234567899999, UNKNOWN_HASH))
+    async def test_eth_get_raw_transaction_by_block_raises_error(
+        self, async_w3: "Web3", unknown_block_num_or_hash: Union[int, HexBytes]
+    ) -> None:
+        with pytest.raises(TransactionNotFound, match=(
+            f"Transaction index: 0 on block id: {to_hex_if_integer(unknown_block_num_or_hash)!r} "
+            f"not found."
+        )):
+            await async_w3.eth.get_raw_transaction_by_block(    # type: ignore
+                unknown_block_num_or_hash, 0
+            )
+
+    @pytest.mark.asyncio
+    async def test_eth_get_raw_transaction_by_block_raises_error_block_identifier(
+        self, async_w3: "Web3"
+    ) -> None:
+        unknown_identifier = "unknown"
+        with pytest.raises(
+            ValueError, match=(
+                f"Value did not match any of the recognized block identifiers: {unknown_identifier}"
+            )
+        ):
+            await async_w3.eth.get_raw_transaction_by_block(unknown_identifier, 0)  # type: ignore
 
     @pytest.mark.asyncio
     async def test_eth_get_balance(self, async_w3: "Web3") -> None:
@@ -2571,8 +2635,59 @@ def test_eth_get_raw_transaction(
         raw_transaction = web3.eth.get_raw_transaction(mined_txn_hash)
         assert is_bytes(raw_transaction)
 
-    def test_eth_get_raw_transaction_raises_error(
-        self, web3: "Web3", mined_txn_hash: HexStr
-    ) -> None:
+    def test_eth_get_raw_transaction_raises_error(self, web3: "Web3") -> None:
         with pytest.raises(TransactionNotFound, match=f"Transaction with hash: '{UNKNOWN_HASH}'"):
             web3.eth.get_raw_transaction(UNKNOWN_HASH)
+
+    def test_eth_get_raw_transaction_by_block(
+        self, web3: "Web3",
+        unlocked_account_dual_type: ChecksumAddress,
+        block_with_txn: BlockData,
+    ) -> None:
+        # eth_getRawTransactionByBlockNumberAndIndex: block identifier
+        # send a txn to make sure pending block has at least one txn
+        web3.eth.send_transaction(
+            {
+                'from': unlocked_account_dual_type,
+                'to': unlocked_account_dual_type,
+                'value': Wei(1),
+            }
+        )
+        last_pending_txn_index = len(web3.eth.get_block('pending')['transactions']) - 1
+        raw_transaction = web3.eth.get_raw_transaction_by_block('pending', last_pending_txn_index)
+        assert is_bytes(raw_transaction)
+
+        # eth_getRawTransactionByBlockNumberAndIndex: block number
+        block_with_txn_number = block_with_txn['number']
+        assert is_integer(block_with_txn_number)
+        raw_transaction = web3.eth.get_raw_transaction_by_block(block_with_txn_number, 0)
+        assert is_bytes(raw_transaction)
+
+        # eth_getRawTransactionByBlockHashAndIndex: block hash
+        block_with_txn_hash = block_with_txn['hash']
+        assert is_bytes(block_with_txn_hash)
+        raw_transaction = web3.eth.get_raw_transaction_by_block(block_with_txn_hash, 0)
+        assert is_bytes(raw_transaction)
+
+    @pytest.mark.parametrize('unknown_block_num_or_hash', (1234567899999, UNKNOWN_HASH))
+    def test_eth_get_raw_transaction_by_block_raises_error(
+        self, web3: "Web3", unknown_block_num_or_hash: Union[int, HexBytes]
+    ) -> None:
+        with pytest.raises(
+            TransactionNotFound, match=(
+                f"Transaction index: 0 on block id: "
+                f"{to_hex_if_integer(unknown_block_num_or_hash)!r} not found."
+            )
+        ):
+            web3.eth.get_raw_transaction_by_block(unknown_block_num_or_hash, 0)
+
+    def test_eth_get_raw_transaction_by_block_raises_error_block_identifier(
+        self, web3: "Web3"
+    ) -> None:
+        unknown_identifier = "unknown"
+        with pytest.raises(
+            ValueError, match=(
+                f"Value did not match any of the recognized block identifiers: {unknown_identifier}"
+            )
+        ):
+            web3.eth.get_raw_transaction_by_block(unknown_identifier, 0)  # type: ignore