Skip to content

Commit

Permalink
Docs and refactoring from discussions on PR ethereum#2457
Browse files Browse the repository at this point in the history
- Add documentation
- Refactor out the provider flag since there is no support for sending transactions w/ CCIP Read support
- Refactor some around the way the ``eth_call`` is made, add flag directly to ``eth_call`` and make ``durin_call()`` and internal ``_durin_call()``
  • Loading branch information
fselmo committed May 25, 2022
1 parent 8800af9 commit 39a4822
Show file tree
Hide file tree
Showing 18 changed files with 418 additions and 214 deletions.
14 changes: 14 additions & 0 deletions docs/contracts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,20 @@ Positional and keyword arguments supplied to the contract function subclass
will be used to find the contract function by signature,
and forwarded to the contract function when applicable.

`EIP-3668 <https://eips.ethereum.org/EIPS/eip-3668>`_ introduced support for the ``OffchainLookup`` revert /
CCIP Read support. The ``ccip_read_enabled`` flag is set to ``True`` for calls by default, as recommended in EIP-3668.
If raising the ``OffchainLookup`` revert is preferred, the flag may be set to ``False`` on a per-call basis.

.. code-block:: python
>>> myContract.functions.revertsWithOffchainLookup(myData).call(ccip_read_enabled=False)
*** web3.exceptions.OffchainLookup
Disabling CCIP Read support can be useful if a transaction needs to be sent to the callback function. In such cases,
"preflighting" with an ``eth_call``, handling the ``OffchainLookup``, and sending the data via a transaction may be
necessary. See :ref:`ccip-read-example` in the examples section for how to preflight a transaction with a contract call.


Methods
~~~~~~~~~~

Expand Down
38 changes: 38 additions & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,44 @@ When someone has an allowance they can transfer those tokens using the
.. _ERC20: https://github.com/ethereum/EIPs/blob/7f4f0377730f5fc266824084188cc17cf246932e/EIPS/eip-20.md


.. _ccip-read-example:

CCIP Read support for offchain lookup
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Contract calls support CCIP Read by default via a ``ccip_read_enabled`` flag that is set to a default value of ``True``.
The following should work by default without raising the ``OffchainLookup`` and instead handling it appropriately as
per the specification outlined in `EIP-3668 <https://eips.ethereum.org/EIPS/eip-3668>`_.

.. code-block:: python
myContract.functions.revertsWithOffchainLookup(myData).call()
If the offchain lookup requires the user to send a transaction rather than make a call, this may be handled
appropriately in the following way:

.. code-block:: python
from web3 import Web3, WebsocketProvider
from web3.utils import handle_offchain_lookup
w3 = Web3(WebsocketProvider(...))
myContract = w3.eth.contract(address=...)
myData = b'data for offchain lookup function call'
# preflight with an `eth_call` and handle the exception
try:
myContract.functions.revertsWithOffchainLookup(myData).call(ccip_read_enabled=False)
except OffchainLookup as ocl:
tx = {'to': myContract.address, 'from': my_account}
data_for_callback_function = handle_offchain_lookup(ocl.payload)
tx['data'] = data_for_callback_function
# send the built transaction with `eth_sendTransaction` or sign and send with `eth_sendRawTransaction`
tx_hash = w3.eth.send_transaction(tx)
Contract Unit Tests in Python
-----------------------------

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Table of Contents
web3.miner
web3.geth
web3.parity
web3.utils
gas_price
ens
constants
Expand Down
11 changes: 10 additions & 1 deletion docs/web3.eth.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1064,7 +1064,7 @@ The following methods are available on the ``web3.eth`` namespace.
.. warning:: Deprecated: This property is deprecated in favor of
:meth:`~web3.eth.Eth.sign_typed_data()`

.. py:method:: Eth.call(transaction, block_identifier=web3.eth.default_block, state_override=None)
.. py:method:: Eth.call(transaction, block_identifier=web3.eth.default_block, state_override=None, ccip_read_enabled=True)
* Delegates to ``eth_call`` RPC Method

Expand All @@ -1090,6 +1090,15 @@ The following methods are available on the ``web3.eth`` namespace.
View their `usage documentation <https://geth.ethereum.org/docs/rpc/ns-eth#3-object---state-override-set>`_
for a list of possible parameters.

`EIP-3668 <https://eips.ethereum.org/EIPS/eip-3668>`_ introduced support for the ``OffchainLookup`` revert / CCIP
Read support. In order to properly handle a call to a contract function that reverts with an ``OffchainLookup``
error for offchain data retrieval, the ``ccip_read_enabled`` flag has been added to the ``eth_call`` method.
``ccip_read_enabled`` is set to ``True`` by default for calls, as recommended in EIP-3668. Therefore, calls to
contract functions that revert with an ``OffchainLookup`` will be handled appropriately by default. If the
``ccip_read_enabled`` flag is set to ``False``, the call will raise the ``OffchainLookup`` instead of properly
handling the exception according to EIP-3668. This may be useful for "preflighting" a transaction call (see
:ref:`ccip-read-example` within the examples section).


.. py:method:: Eth.fee_history(block_count, newest_block, reward_percentiles=None)
Expand Down
17 changes: 17 additions & 0 deletions docs/web3.utils.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Utils
=====

.. py:module:: web3.utils
The ``utils`` module houses public utility and helper functions.


.. py:method:: Utils.handle_offchain_lookup(offchain_lookup_payload, transaction)
Handle ``OffchainLookup`` reverts on contract function calls manually. For an example, see :ref:`ccip-read-example`
within the examples section.


.. py:method:: Utils.async_handle_offchain_lookup(offchain_lookup_payload, transaction)
The async version of the ``handle_offchain_lookup()`` utility method described above.
2 changes: 1 addition & 1 deletion tests/core/contracts/test_offchain_lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from web3._utils.module_testing.utils import (
mock_offchain_lookup_request_response,
)
from web3._utils.type_conversion_utils import (
from web3._utils.type_conversion import (
to_hex_if_bytes,
)
from web3.exceptions import (
Expand Down
3 changes: 1 addition & 2 deletions tests/core/web3-module/test_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,5 @@ def test_auto_provider_none():
type(w3.provider) == AutoProvider


def test_provider_default_values_for_ccip_read(w3):
assert w3.provider.ccip_read_enabled
def test_provider_default_value_for_ccip_read_redirect(w3):
assert w3.provider.ccip_read_max_redirects == 4
40 changes: 8 additions & 32 deletions tests/ens/test_offchain_resolution.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import pytest

from eth_abi.abi import (
decode_abi,
)
from eth_utils import (
to_checksum_address,
)
import requests

from ens.utils import (
Expand Down Expand Up @@ -113,37 +107,19 @@ def mock_get(*args, **_):
ens.address('offchainexample.eth')


def test_offchain_resolver_function_call_with_ccip_read_enabled(ens, monkeypatch):
def mock_get(*args, **kwargs):
return MockHttpSuccessResponse('get', *args, **kwargs)

monkeypatch.setattr(requests.Session, 'get', mock_get)

def test_offchain_resolver_function_call_raises_with_ccip_read_disabled(ens, monkeypatch):
offchain_resolver = ens.resolver('offchainexample.eth')

# set global ccip_read_enabled flag on provider to False
ens.w3.provider.ccip_read_enabled = False

# should fail here with global provider flag set to False
# should fail here with `ccip_read_enabled` flag set to False
with pytest.raises(OffchainLookup):
offchain_resolver.functions.resolve(
ens_encode_name('offchainexample.eth'),
ENCODED_ADDR_CALLDATA,
).call()

# pass flag on specific call should work
resolved_via_function_call = offchain_resolver.functions.resolve(
ens_encode_name('offchainexample.eth'),
ENCODED_ADDR_CALLDATA,
).call(ccip_read_enabled=True)
).call(ccip_read_enabled=False)

# pass flag on specific call via ContractCaller is also an option
resolved_via_caller = offchain_resolver.caller(ccip_read_enabled=True).resolve(
ens_encode_name('offchainexample.eth'),
ENCODED_ADDR_CALLDATA,
)

assert resolved_via_caller == resolved_via_function_call

decoded_result = decode_abi(['address'], resolved_via_caller)[0]
assert to_checksum_address(decoded_result) == EXPECTED_RESOLVED_ADDRESS
with pytest.raises(OffchainLookup):
offchain_resolver.caller(ccip_read_enabled=False).resolve(
ens_encode_name('offchainexample.eth'),
ENCODED_ADDR_CALLDATA,
)
45 changes: 30 additions & 15 deletions web3/_utils/module_testing/eth_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
mine_pending_block,
mock_offchain_lookup_request_response,
)
from web3._utils.type_conversion_utils import (
from web3._utils.type_conversion import (
to_hex_if_bytes,
)
from web3.exceptions import (
Expand All @@ -55,6 +55,7 @@
InvalidAddress,
InvalidTransaction,
NameNotFound,
OffchainLookup,
TimeExhausted,
TooManyRequests,
TransactionNotFound,
Expand All @@ -81,6 +82,7 @@
)

UNKNOWN_ADDRESS = ChecksumAddress(HexAddress(HexStr('0xdEADBEeF00000000000000000000000000000000')))

UNKNOWN_HASH = HexStr('0xdeadbeef00000000000000000000000000000000000000000000000000000000')
# "test offchain lookup" as an abi-encoded string
OFFCHAIN_LOOKUP_TEST_DATA = '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001474657374206f6666636861696e206c6f6f6b7570000000000000000000000000' # noqa: E501
Expand Down Expand Up @@ -772,12 +774,23 @@ async def test_eth_call_offchain_lookup(
'to': offchain_lookup_contract.address,
'data': '0x6337ed58000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001474657374206f6666636861696e206c6f6f6b7570000000000000000000000000' # noqa: E501
}
async_w3.provider.ccip_read_enabled = True
response = await async_w3.eth.call(tx) # type: ignore
response_as_bytes = async_w3.codec.decode_abi(['bytes'], response)[0]
decoded_as_string = async_w3.codec.decode_abi(['string'], response_as_bytes)[0]
assert decoded_as_string == 'web3py'

@pytest.mark.asyncio
async def test_eth_call_offchain_lookup_raises_when_ccip_read_is_disabled(
self, async_w3: "Web3", offchain_lookup_contract: "Contract",
) -> None:
# TODO: change to contract call when async Contract is supported
tx = {
'to': offchain_lookup_contract.address,
'data': '0x6337ed58000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001474657374206f6666636861696e206c6f6f6b7570000000000000000000000000' # noqa: E501
}
with pytest.raises(OffchainLookup):
await async_w3.eth.call(tx, ccip_read_enabled=False) # type: ignore

@pytest.mark.asyncio
@pytest.mark.parametrize("max_redirects", range(-4, 4))
async def test_eth_call_offchain_lookup_raises_if_max_redirects_is_less_than_4(
Expand All @@ -791,7 +804,6 @@ async def test_eth_call_offchain_lookup_raises_if_max_redirects_is_less_than_4(
'to': offchain_lookup_contract.address,
'data': '0x6337ed58000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001474657374206f6666636861696e206c6f6f6b7570000000000000000000000000' # noqa: E501
}
async_w3.provider.ccip_read_enabled = True
async_w3.provider.ccip_read_max_redirects = max_redirects
with pytest.raises(ValueError, match="at least 4"):
await async_w3.eth.call(tx) # type: ignore
Expand All @@ -818,7 +830,6 @@ async def test_eth_call_offchain_lookup_raises_for_improperly_formatted_rest_req
'to': offchain_lookup_contract.address,
'data': '0x6337ed58000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001474657374206f6666636861696e206c6f6f6b7570000000000000000000000000' # noqa: E501
}
async_w3.provider.ccip_read_enabled = True
with pytest.raises(ValidationError, match="missing 'data' field"):
await async_w3.eth.call(tx) # type: ignore

Expand Down Expand Up @@ -858,7 +869,6 @@ async def test_eth_call_offchain_lookup_tries_next_url_for_non_4xx_error_status_
'to': offchain_lookup_contract.address,
'data': '0x6337ed58000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001474657374206f6666636861696e206c6f6f6b7570000000000000000000000000' # noqa: E501
}
async_w3.provider.ccip_read_enabled = True
response = await async_w3.eth.call(tx) # type: ignore
response_as_bytes = async_w3.codec.decode_abi(['bytes'], response)[0]
decoded_as_string = async_w3.codec.decode_abi(['string'], response_as_bytes)[0]
Expand Down Expand Up @@ -887,7 +897,6 @@ async def test_eth_call_offchain_lookup_calls_raise_for_status_for_4xx_status_co
'to': offchain_lookup_contract.address,
'data': '0x6337ed58000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001474657374206f6666636861696e206c6f6f6b7570000000000000000000000000' # noqa: E501
}
async_w3.provider.ccip_read_enabled = True
await async_w3.eth.call(tx) # type: ignore

@pytest.mark.asyncio
Expand All @@ -901,7 +910,6 @@ async def test_eth_call_offchain_lookup_raises_when_all_supplied_urls_fail(
'to': offchain_lookup_contract.address,
'data': '0x6337ed58000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001474657374206f6666636861696e206c6f6f6b7570000000000000000000000000' # noqa: E501
}
async_w3.provider.ccip_read_enabled = True
await async_w3.eth.call(tx) # type: ignore

@pytest.mark.asyncio
Expand All @@ -920,7 +928,6 @@ async def test_eth_call_continuous_offchain_lookup_raises_with_too_many_requests
with pytest.raises(TooManyRequests, match="Too many CCIP read redirects"):
# TODO: change to contract call when async Contract is supported
tx = {'to': offchain_lookup_contract.address, 'data': '0x09a3c01b'}
async_w3.provider.ccip_read_enabled = True
await async_w3.eth.call(tx) # type: ignore

@pytest.mark.asyncio
Expand Down Expand Up @@ -2484,9 +2491,17 @@ def test_eth_call_offchain_lookup(
)
response = offchain_lookup_contract.functions.testOffchainLookup(
OFFCHAIN_LOOKUP_TEST_DATA
).call(ccip_read_enabled=True)
).call()
assert w3.codec.decode_abi(['string'], response)[0] == 'web3py'

def test_eth_call_offchain_lookup_raises_when_ccip_read_is_disabled(
self, w3: "Web3", offchain_lookup_contract: "Contract",
) -> None:
with pytest.raises(OffchainLookup):
offchain_lookup_contract.functions.testOffchainLookup(
OFFCHAIN_LOOKUP_TEST_DATA
).call(ccip_read_enabled=False)

@pytest.mark.parametrize("max_redirects", range(-4, 4))
def test_eth_call_offchain_lookup_raises_if_max_redirects_is_less_than_4(
self,
Expand All @@ -2500,7 +2515,7 @@ def test_eth_call_offchain_lookup_raises_if_max_redirects_is_less_than_4(
with pytest.raises(ValueError, match="at least 4"):
offchain_lookup_contract.functions.testOffchainLookup(
OFFCHAIN_LOOKUP_TEST_DATA
).call(ccip_read_enabled=True)
).call()

w3.provider.ccip_read_max_redirects = default_max_redirects # cleanup

Expand All @@ -2521,7 +2536,7 @@ def test_eth_call_offchain_lookup_raises_for_improperly_formatted_rest_request_r
with pytest.raises(ValidationError, match="missing 'data' field"):
offchain_lookup_contract.functions.testOffchainLookup(
OFFCHAIN_LOOKUP_TEST_DATA
).call(ccip_read_enabled=True)
).call()

@pytest.mark.parametrize('status_code_non_4xx_error', [100, 300, 500, 600])
def test_eth_call_offchain_lookup_tries_next_url_for_non_4xx_error_status_and_tests_POST(
Expand Down Expand Up @@ -2555,7 +2570,7 @@ def test_eth_call_offchain_lookup_tries_next_url_for_non_4xx_error_status_and_te
)
response = offchain_lookup_contract.functions.testOffchainLookup(
OFFCHAIN_LOOKUP_TEST_DATA
).call(ccip_read_enabled=True)
).call()
assert w3.codec.decode_abi(['string'], response)[0] == 'web3py'

@pytest.mark.parametrize('status_code_4xx_error', [400, 410, 450, 499])
Expand All @@ -2577,7 +2592,7 @@ def test_eth_call_offchain_lookup_calls_raise_for_status_for_4xx_status_code(
with pytest.raises(Exception, match="called raise_for_status\\(\\)"):
offchain_lookup_contract.functions.testOffchainLookup(
OFFCHAIN_LOOKUP_TEST_DATA
).call(ccip_read_enabled=True)
).call()

def test_eth_call_offchain_lookup_raises_when_all_supplied_urls_fail(
self, w3: "Web3", offchain_lookup_contract: "Contract",
Expand All @@ -2586,7 +2601,7 @@ def test_eth_call_offchain_lookup_raises_when_all_supplied_urls_fail(
with pytest.raises(Exception, match="Offchain lookup failed for supplied urls"):
offchain_lookup_contract.functions.testOffchainLookup(
OFFCHAIN_LOOKUP_TEST_DATA
).call(ccip_read_enabled=True)
).call()

def test_eth_call_continuous_offchain_lookup_raises_with_too_many_requests(
self,
Expand All @@ -2601,7 +2616,7 @@ def test_eth_call_continuous_offchain_lookup_raises_with_too_many_requests(
mocked_request_url=f'https://web3.py/gateway/{normalized_contract_address}/0x.json',
)
with pytest.raises(TooManyRequests, match="Too many CCIP read redirects"):
offchain_lookup_contract.caller(ccip_read_enabled=True).continuousOffchainLookup()
offchain_lookup_contract.caller().continuousOffchainLookup()

def test_eth_estimate_gas_revert_with_msg(
self,
Expand Down
Loading

0 comments on commit 39a4822

Please sign in to comment.