Skip to content

Commit 46c1600

Browse files
committed
Docs and refactoring from discussions on PR ethereum#2457
- 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()``
1 parent 2a2ac7a commit 46c1600

19 files changed

+362
-267
lines changed

docs/contracts.rst

+14
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,20 @@ Positional and keyword arguments supplied to the contract function subclass
743743
will be used to find the contract function by signature,
744744
and forwarded to the contract function when applicable.
745745

746+
`EIP-3668 <https://eips.ethereum.org/EIPS/eip-3668>`_ introduced support for the ``OffchainLookup`` revert /
747+
CCIP Read support. The ``ccip_read_enabled`` flag is set to ``True`` for calls by default, as recommended in EIP-3668.
748+
If raising the ``OffchainLookup`` revert is preferred, the flag may be set to ``False`` on a per-call basis.
749+
750+
.. code-block:: python
751+
752+
>>> myContract.functions.revertsWithOffchainLookup(myData).call(ccip_read_enabled=False)
753+
*** web3.exceptions.OffchainLookup
754+
755+
Disabling CCIP Read support can be useful if a transaction needs to be sent to the callback function. In such cases,
756+
"preflighting" with an ``eth_call``, handling the ``OffchainLookup``, and sending the data via a transaction may be
757+
necessary. See :ref:`ccip-read-example` in the examples section for how to preflight a transaction with a contract call.
758+
759+
746760
Methods
747761
~~~~~~~~~~
748762

docs/examples.rst

+38
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,44 @@ When someone has an allowance they can transfer those tokens using the
624624
.. _ERC20: https://github.com/ethereum/EIPs/blob/7f4f0377730f5fc266824084188cc17cf246932e/EIPS/eip-20.md
625625

626626

627+
.. _ccip-read-example:
628+
629+
CCIP Read support for offchain lookup
630+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
631+
632+
Contract calls support CCIP Read by default via a ``ccip_read_enabled`` flag that is set to a default value of ``True``.
633+
The following should work by default without raising the ``OffchainLookup`` and instead handling it appropriately as
634+
per the specification outlined in `EIP-3668 <https://eips.ethereum.org/EIPS/eip-3668>`_.
635+
636+
.. code-block:: python
637+
638+
myContract.functions.revertsWithOffchainLookup(myData).call()
639+
640+
If the offchain lookup requires the user to send a transaction rather than make a call, this may be handled
641+
appropriately in the following way:
642+
643+
.. code-block:: python
644+
645+
from web3 import Web3, WebsocketProvider
646+
from web3.utils import handle_offchain_lookup
647+
648+
w3 = Web3(WebsocketProvider(...))
649+
650+
myContract = w3.eth.contract(address=...)
651+
myData = b'data for offchain lookup function call'
652+
653+
# preflight with an `eth_call` and handle the exception
654+
try:
655+
myContract.functions.revertsWithOffchainLookup(myData).call(ccip_read_enabled=False)
656+
except OffchainLookup as ocl:
657+
tx = {'to': myContract.address, 'from': my_account}
658+
data_for_callback_function = handle_offchain_lookup(ocl.payload)
659+
tx['data'] = data_for_callback_function
660+
661+
# send the built transaction with `eth_sendTransaction` or sign and send with `eth_sendRawTransaction`
662+
tx_hash = w3.eth.send_transaction(tx)
663+
664+
627665
Contract Unit Tests in Python
628666
-----------------------------
629667

docs/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Table of Contents
6767
web3.miner
6868
web3.geth
6969
web3.parity
70+
web3.utils
7071
gas_price
7172
ens
7273
constants

docs/web3.eth.rst

+10-1
Original file line numberDiff line numberDiff line change
@@ -1064,7 +1064,7 @@ The following methods are available on the ``web3.eth`` namespace.
10641064
.. warning:: Deprecated: This property is deprecated in favor of
10651065
:meth:`~web3.eth.Eth.sign_typed_data()`
10661066

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

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

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

10941103
.. py:method:: Eth.fee_history(block_count, newest_block, reward_percentiles=None)
10951104

docs/web3.utils.rst

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Utils
2+
=====
3+
4+
.. py:module:: web3.utils
5+
6+
The ``utils`` module houses public utility and helper functions.
7+
8+
9+
.. py:method:: Utils.handle_offchain_lookup(offchain_lookup_payload, transaction)
10+
11+
Handle ``OffchainLookup`` reverts on contract function calls manually. For an example, see :ref:`ccip-read-example`
12+
within the examples section.
13+
14+
15+
.. py:method:: Utils.async_handle_offchain_lookup(offchain_lookup_payload, transaction)
16+
17+
The async version of the ``handle_offchain_lookup()`` utility method described above.

tests/core/contracts/test_offchain_lookup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from web3._utils.module_testing.utils import (
1313
mock_offchain_lookup_request_response,
1414
)
15-
from web3._utils.type_conversion_utils import (
15+
from web3._utils.type_conversion import (
1616
to_hex_if_bytes,
1717
)
1818
from web3.exceptions import (

tests/core/web3-module/test_providers.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,5 @@ def test_auto_provider_none():
2525
type(w3.provider) == AutoProvider
2626

2727

28-
def test_provider_default_values_for_ccip_read(w3):
29-
assert w3.provider.ccip_read_enabled
28+
def test_provider_default_value_for_ccip_read_redirect(w3):
3029
assert w3.provider.ccip_read_max_redirects == 4

tests/ens/test_offchain_resolution.py

+8-32
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
import pytest
22

3-
from eth_abi.abi import (
4-
decode_abi,
5-
)
6-
from eth_utils import (
7-
to_checksum_address,
8-
)
93
import requests
104

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

115109

116-
def test_offchain_resolver_function_call_with_ccip_read_enabled(ens, monkeypatch):
117-
def mock_get(*args, **kwargs):
118-
return MockHttpSuccessResponse('get', *args, **kwargs)
119-
120-
monkeypatch.setattr(requests.Session, 'get', mock_get)
121-
110+
def test_offchain_resolver_function_call_raises_with_ccip_read_disabled(ens, monkeypatch):
122111
offchain_resolver = ens.resolver('offchainexample.eth')
123112

124-
# set global ccip_read_enabled flag on provider to False
125-
ens.w3.provider.ccip_read_enabled = False
126-
127-
# should fail here with global provider flag set to False
113+
# should fail here with `ccip_read_enabled` flag set to False
128114
with pytest.raises(OffchainLookup):
129115
offchain_resolver.functions.resolve(
130116
ens_encode_name('offchainexample.eth'),
131117
ENCODED_ADDR_CALLDATA,
132-
).call()
133-
134-
# pass flag on specific call should work
135-
resolved_via_function_call = offchain_resolver.functions.resolve(
136-
ens_encode_name('offchainexample.eth'),
137-
ENCODED_ADDR_CALLDATA,
138-
).call(ccip_read_enabled=True)
118+
).call(ccip_read_enabled=False)
139119

140120
# pass flag on specific call via ContractCaller is also an option
141-
resolved_via_caller = offchain_resolver.caller(ccip_read_enabled=True).resolve(
142-
ens_encode_name('offchainexample.eth'),
143-
ENCODED_ADDR_CALLDATA,
144-
)
145-
146-
assert resolved_via_caller == resolved_via_function_call
147-
148-
decoded_result = decode_abi(['address'], resolved_via_caller)[0]
149-
assert to_checksum_address(decoded_result) == EXPECTED_RESOLVED_ADDRESS
121+
with pytest.raises(OffchainLookup):
122+
offchain_resolver.caller(ccip_read_enabled=False).resolve(
123+
ens_encode_name('offchainexample.eth'),
124+
ENCODED_ADDR_CALLDATA,
125+
)

web3/_utils/async_transactions.py

-85
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,9 @@
11
from typing import (
22
TYPE_CHECKING,
3-
Any,
4-
Dict,
53
Optional,
64
cast,
75
)
86

9-
from eth_abi import (
10-
encode_abi,
11-
)
12-
from eth_typing import (
13-
URI,
14-
)
15-
16-
from web3._utils.request import (
17-
async_get_json_from_client_response,
18-
async_get_response_from_get_request,
19-
async_get_response_from_post_request,
20-
)
21-
from web3._utils.type_conversion_utils import (
22-
to_bytes_if_hex,
23-
to_hex_if_bytes,
24-
)
25-
from web3.exceptions import (
26-
ValidationError,
27-
)
287
from web3.types import (
298
BlockIdentifier,
309
TxParams,
@@ -61,67 +40,3 @@ async def get_buffered_gas_estimate(
6140
)
6241

6342
return min(gas_limit, gas_estimate + gas_buffer)
64-
65-
66-
async def async_handle_offchain_lookup(
67-
offchain_lookup_payload: Dict[str, Any],
68-
transaction: TxParams,
69-
) -> bytes:
70-
formatted_sender = to_hex_if_bytes(offchain_lookup_payload['sender']).lower()
71-
formatted_data = to_hex_if_bytes(offchain_lookup_payload['callData']).lower()
72-
73-
if formatted_sender != to_hex_if_bytes(transaction['to']).lower():
74-
raise ValidationError(
75-
'Cannot handle OffchainLookup raised inside nested call. Returned `sender` '
76-
'value does not equal `to` address in transaction.'
77-
)
78-
79-
for url in offchain_lookup_payload['urls']:
80-
formatted_url = URI(
81-
str(url)
82-
.replace('{sender}', str(formatted_sender))
83-
.replace('{data}', str(formatted_data))
84-
)
85-
86-
try:
87-
if '{data}' in url and '{sender}' in url:
88-
response = await async_get_response_from_get_request(formatted_url)
89-
elif '{sender}' in url:
90-
response = await async_get_response_from_post_request(formatted_url, data={
91-
"data": formatted_data,
92-
"sender": formatted_sender
93-
})
94-
else:
95-
raise ValidationError('url not formatted properly.')
96-
except Exception:
97-
continue # try next url if timeout or issues making the request
98-
99-
if 400 <= response.status <= 499: # if request returns 400 error, raise exception
100-
response.raise_for_status()
101-
if not 200 <= response.status <= 299: # if not 400 error, try next url
102-
continue
103-
104-
result = await async_get_json_from_client_response(response)
105-
106-
if 'data' not in result.keys():
107-
raise ValidationError(
108-
"Improperly formatted response for offchain lookup HTTP request - missing 'data' "
109-
"field."
110-
)
111-
112-
encoded_data_with_function_selector = b''.join([
113-
# 4-byte callback function selector
114-
to_bytes_if_hex(offchain_lookup_payload['callbackFunction']),
115-
116-
# encode the `data` from the result and the `extraData` as bytes
117-
encode_abi(
118-
['bytes', 'bytes'],
119-
[
120-
to_bytes_if_hex(result['data']),
121-
to_bytes_if_hex(offchain_lookup_payload['extraData']),
122-
]
123-
)
124-
])
125-
126-
return encoded_data_with_function_selector
127-
raise Exception("Offchain lookup failed for supplied urls.")

0 commit comments

Comments
 (0)