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 20, 2022
1 parent 2a2ac7a commit 8bfacba
Show file tree
Hide file tree
Showing 16 changed files with 317 additions and 251 deletions.
10 changes: 10 additions & 0 deletions docs/contracts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,16 @@ 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
Methods
~~~~~~~~~~

Expand Down
36 changes: 36 additions & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,42 @@ 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 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 either `eth_sendTransaction` or sign and send with `eth_sendRawTransaction`
tx_hash = w3.eth.send_transaction(tx)
Contract Unit Tests in Python
-----------------------------

Expand Down
8 changes: 8 additions & 0 deletions docs/web3.eth.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1090,6 +1090,14 @@ 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 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 <https://eips.ethereum.org/EIPS/eip-3668>`_, thus 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.


.. py:method:: Eth.fee_history(block_count, newest_block, reward_percentiles=None)
Expand Down
6 changes: 3 additions & 3 deletions tests/core/contracts/test_offchain_lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
from web3._utils.module_testing.utils import (
mock_offchain_lookup_request_response,
)
from web3._utils.type_conversion_utils import (
to_hex_if_bytes,
)
from web3.exceptions import (
TooManyRequests,
ValidationError,
)
from web3.utils import (
to_hex_if_bytes,
)

# "test offchain lookup" as an abi-encoded string
OFFCHAIN_LOOKUP_TEST_DATA = '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001474657374206f6666636861696e206c6f6f6b7570000000000000000000000000' # noqa: E501
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,
)
85 changes: 0 additions & 85 deletions web3/_utils/async_transactions.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,9 @@
from typing import (
TYPE_CHECKING,
Any,
Dict,
Optional,
cast,
)

from eth_abi import (
encode_abi,
)
from eth_typing import (
URI,
)

from web3._utils.request import (
async_get_json_from_client_response,
async_get_response_from_get_request,
async_get_response_from_post_request,
)
from web3._utils.type_conversion_utils import (
to_bytes_if_hex,
to_hex_if_bytes,
)
from web3.exceptions import (
ValidationError,
)
from web3.types import (
BlockIdentifier,
TxParams,
Expand Down Expand Up @@ -61,67 +40,3 @@ async def get_buffered_gas_estimate(
)

return min(gas_limit, gas_estimate + gas_buffer)


async def async_handle_offchain_lookup(
offchain_lookup_payload: Dict[str, Any],
transaction: TxParams,
) -> bytes:
formatted_sender = to_hex_if_bytes(offchain_lookup_payload['sender']).lower()
formatted_data = to_hex_if_bytes(offchain_lookup_payload['callData']).lower()

if formatted_sender != to_hex_if_bytes(transaction['to']).lower():
raise ValidationError(
'Cannot handle OffchainLookup raised inside nested call. Returned `sender` '
'value does not equal `to` address in transaction.'
)

for url in offchain_lookup_payload['urls']:
formatted_url = URI(
str(url)
.replace('{sender}', str(formatted_sender))
.replace('{data}', str(formatted_data))
)

try:
if '{data}' in url and '{sender}' in url:
response = await async_get_response_from_get_request(formatted_url)
elif '{sender}' in url:
response = await async_get_response_from_post_request(formatted_url, data={
"data": formatted_data,
"sender": formatted_sender
})
else:
raise ValidationError('url not formatted properly.')
except Exception:
continue # try next url if timeout or issues making the request

if 400 <= response.status <= 499: # if request returns 400 error, raise exception
response.raise_for_status()
if not 200 <= response.status <= 299: # if not 400 error, try next url
continue

result = await async_get_json_from_client_response(response)

if 'data' not in result.keys():
raise ValidationError(
"Improperly formatted response for offchain lookup HTTP request - missing 'data' "
"field."
)

encoded_data_with_function_selector = b''.join([
# 4-byte callback function selector
to_bytes_if_hex(offchain_lookup_payload['callbackFunction']),

# encode the `data` from the result and the `extraData` as bytes
encode_abi(
['bytes', 'bytes'],
[
to_bytes_if_hex(result['data']),
to_bytes_if_hex(offchain_lookup_payload['extraData']),
]
)
])

return encoded_data_with_function_selector
raise Exception("Offchain lookup failed for supplied urls.")
Loading

0 comments on commit 8bfacba

Please sign in to comment.