Skip to content

Commit 8bfacba

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 8bfacba

16 files changed

+317
-251
lines changed

docs/contracts.rst

+10
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,16 @@ 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+
746756
Methods
747757
~~~~~~~~~~
748758

docs/examples.rst

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

docs/web3.eth.rst

+8
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,14 @@ 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 introduced support for the ``OffchainLookup`` revert / CCIP Read support. In order to properly handle a
1094+
call to a contract function that reverts with an ``OffchainLookup`` error for offchain data retrieval, the
1095+
``ccip_read_enabled`` flag has been added to the ``eth_call`` method. ``ccip_read_enabled`` is set to ``True`` by
1096+
default for calls, as recommended in `EIP-3668 <https://eips.ethereum.org/EIPS/eip-3668>`_, thus calls to contract
1097+
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.
1100+
10931101

10941102
.. py:method:: Eth.fee_history(block_count, newest_block, reward_percentiles=None)
10951103

tests/core/contracts/test_offchain_lookup.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@
1212
from web3._utils.module_testing.utils import (
1313
mock_offchain_lookup_request_response,
1414
)
15-
from web3._utils.type_conversion_utils import (
16-
to_hex_if_bytes,
17-
)
1815
from web3.exceptions import (
1916
TooManyRequests,
2017
ValidationError,
2118
)
19+
from web3.utils import (
20+
to_hex_if_bytes,
21+
)
2222

2323
# "test offchain lookup" as an abi-encoded string
2424
OFFCHAIN_LOOKUP_TEST_DATA = '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001474657374206f6666636861696e206c6f6f6b7570000000000000000000000000' # noqa: E501

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)