Migrate Python libraries to v3 (#2284)
* .gitignore migrations/0x_ganache_snapshot
* .gitignore new-ish Python contract wrappers
These should have been added back when we started generating these
wrappers.
* rm superfluous contract artifact in Python package
All of the contract artifacts were removed from the Python package
recently, because now they're copied from the monorepo/packages area as
an automated build step. Somehow this one artifact slipped through the
cracks.
* Eliminate circular dependency
This was preventing the Exchange wrapper from ever importing its
validator!
* Improve output of monorepo-level parallel script
- Capture stderr (and have it included in stdout) so that it doesn't
leak onto the console for commands that didn't actually fail.
- Include all error output in the Exception object (eliminate print
statement).
* Silence new versions of linters
Newer versions care about this stuff. Old versions didn't, and we don't
either.
* Support Rich Reverts via Web3.py middleware
* Fix bug in generated wrappers' bytes handling
`bytes.fromhex(bytes.decode('utf-8')` is just plain wrong. It would
work for some cases, but is not working when trying to fill orders with
the latest Exchange contract.
* Migrate to Exchange v3
* Fix typo in DevUtils documentation
* Include new contracts in docs
* Re-enable Python checks in CI
* Accept strings for bytes
* Fix CircleCI build artifacts for gen'd python
I swear the previous way was working before, but it wasn't working now,
so this fixes it.
* Accept a provider OR a Web3 object
In various places. This allows the caller to install middleware (which
in web3.py is installed on a Web3 object, not on a provider) before
executing any RPC calls, which is important for the case where one wants
to produce signatures locally before submitting to a remote node.
* wrapper base: don't assume there are accounts
* Eliminate some inline linter directives
* make CHANGELOGs be REVERSE chronological
* Update CHANGELOG entries and bump version numbers
* @0x/contract-addresses: Put addr's in JSON, not TS
This allows easier consumption by other languages. (Specifically, it
eliminates the overhead of keeping the Python addresses package in sync
with the TypeScript one.)
* sra_client.py: incl. docker in `./setup.py clean`
* sra_client.py: Migrate to protocol v3
Removed script that existed only to exclude runs of sra_client builds
(parallel_without_sra_client). Now `parallel` is used by CI,
re-including sra_client in CI checks.
* abi-gen/templates/Py: clarify if/else logic
In response to
https://github.com/0xProject/0x-monorepo/pull/2284#discussion_r342200906
* sra_client.py: Update CHANGELOG and bump version
* contract_addresses/setup.py: rm unnecessary rm
* json_schemas.py: corrections to dev dependencies
* In tests against deployment, also run doctests
* contract_wrappers example: rm xtra Order attribute
Thanks to @steveklebanoff for catching this.
https://github.com/0xProject/0x-monorepo/pull/2284#pullrequestreview-312065368
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## 2.0.0 - TBD
|
||||
|
||||
- Updated for version 3 of the protocol.
|
||||
- Allow wrappers to be instantiated with EITHER a Web3.py `BaseProvider` OR an already-instantiated `Web3` client object.
|
||||
- Accept `str`ing arguments to `bytes` contract method parameters.
|
||||
- Expanded documentation examples.
|
||||
- Moved methods `jsdict_to_order()` and `order_to_jsdict()` from `zero_ex.contract_wrappers.exchange.types` to `zero_ex.contract_wrappers.order_conversions`.
|
||||
- Changed field name `zero_ex.contract_wrappers.tx_params.TxParams.gasPrice` to `.gas_price`.
|
||||
|
||||
## 1.1.0 - 2019-08-14
|
||||
|
||||
- Added wrapper for DevUtils contract.
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
"""setuptools module for contract_wrappers package."""
|
||||
|
||||
# pylint: disable=import-outside-toplevel
|
||||
# we import things outside of top-level because 3rd party libs may not yet be
|
||||
# installed when you invoke this script
|
||||
|
||||
import subprocess # nosec
|
||||
from shutil import rmtree
|
||||
from os import environ, path, remove
|
||||
from pathlib import Path
|
||||
from sys import argv
|
||||
from sys import argv, exit # pylint: disable=redefined-builtin
|
||||
|
||||
from distutils.command.clean import clean
|
||||
import distutils.command.build_py
|
||||
@@ -192,7 +196,7 @@ with open("README.md", "r") as file_handle:
|
||||
|
||||
setup(
|
||||
name="0x-contract-wrappers",
|
||||
version="1.1.0",
|
||||
version="2.0.0",
|
||||
description="Python wrappers for 0x smart contracts",
|
||||
long_description=README_MD,
|
||||
long_description_content_type="text/markdown",
|
||||
|
||||
@@ -34,7 +34,7 @@ zero_ex.contract_wrappers.coordinator_registry
|
||||
|
||||
|
||||
zero_ex.contract_wrappers.dev_utils
|
||||
=======================================
|
||||
===================================
|
||||
|
||||
.. automodule:: zero_ex.contract_wrappers.dev_utils
|
||||
:members:
|
||||
@@ -49,6 +49,22 @@ zero_ex.contract_wrappers.dutch_auction
|
||||
:special-members:
|
||||
|
||||
|
||||
zero_ex.contract_wrappers.erc1155_mintable
|
||||
==========================================
|
||||
|
||||
.. automodule:: zero_ex.contract_wrappers.erc1155_mintable
|
||||
:members:
|
||||
:special-members:
|
||||
|
||||
|
||||
zero_ex.contract_wrappers.erc1155_proxy
|
||||
=======================================
|
||||
|
||||
.. automodule:: zero_ex.contract_wrappers.erc1155_proxy
|
||||
:members:
|
||||
:special-members:
|
||||
|
||||
|
||||
zero_ex.contract_wrappers.erc20_proxy
|
||||
=====================================
|
||||
|
||||
@@ -145,6 +161,14 @@ zero_ex.contract_wrappers.order_validator
|
||||
:special-members:
|
||||
|
||||
|
||||
zero_ex.contract_wrappers.static_call_proxy
|
||||
===========================================
|
||||
|
||||
.. automodule:: zero_ex.contract_wrappers.static_call_proxy
|
||||
:members:
|
||||
:special-members:
|
||||
|
||||
|
||||
zero_ex.contract_wrappers.weth9
|
||||
===============================
|
||||
|
||||
@@ -180,18 +204,20 @@ zero_ex.contract_wrappers.exchange.types
|
||||
|
||||
.. autoclass:: zero_ex.contract_wrappers.exchange.types.MatchedFillResults
|
||||
|
||||
.. autoclass:: zero_ex.contract_wrappers.exchange.types.ZeroExTransaction
|
||||
|
||||
zero_ex.contract_wrappers.exchange: Generated Tuples
|
||||
====================================================
|
||||
|
||||
.. autoclass:: zero_ex.contract_wrappers.exchange.Tuple0x260219a2
|
||||
.. autoclass:: zero_ex.contract_wrappers.exchange.Tuple0x6ca34a6f
|
||||
|
||||
This is the generated class representing `the Order struct <https://0x.org/docs/contracts#structs-Order>`_.
|
||||
|
||||
.. autoclass:: zero_ex.contract_wrappers.exchange.Tuple0xbb41e5b3
|
||||
.. autoclass:: zero_ex.contract_wrappers.exchange.Tuple0x735c43e3
|
||||
|
||||
This is the generated class representing `the FillResults struct <https://0x.org/docs/contracts#structs-FillResults>`_.
|
||||
|
||||
.. autoclass:: zero_ex.contract_wrappers.exchange.Tuple0x054ca44e
|
||||
.. autoclass:: zero_ex.contract_wrappers.exchange.Tuple0x4c5ca29b
|
||||
|
||||
This is the generated class representing `the MatchedFillResults struct <https://0x.org/docs/contracts#structs-MatchedFillResults>`_.
|
||||
|
||||
@@ -199,6 +225,10 @@ zero_ex.contract_wrappers.exchange: Generated Tuples
|
||||
|
||||
This is the generated class representing `the OrderInfo struct <https://0x.org/docs/contracts#structs-OrderInfo>`_.
|
||||
|
||||
.. autoclass:: zero_ex.contract_wrappers.exchange.Tuple0xdabc15fe
|
||||
|
||||
This is the generated class representing `the ZeroExTransaction struct <https://0x.org/docs/contracts#structs-ZeroExTransaction>`_.
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
"""0x Python API."""
|
||||
__import__("pkg_resources").declare_namespace(__name__)
|
||||
__import__("pkg_resources").declare_namespace(__name__) # type: ignore
|
||||
|
||||
@@ -52,10 +52,10 @@ the addresses of the 0x contracts on each network, including those that come
|
||||
pre-deployed deployed in the `0xorg/ganache-cli`:code: docker image. Let's
|
||||
capture the addresses we'll use throughout the examples below:
|
||||
|
||||
>>> from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES, NetworkId
|
||||
>>> weth_address = NETWORK_TO_ADDRESSES[NetworkId.GANACHE].ether_token
|
||||
>>> zrx_address = NETWORK_TO_ADDRESSES[NetworkId.GANACHE].zrx_token
|
||||
>>> exchange_address = NETWORK_TO_ADDRESSES[NetworkId.GANACHE].exchange
|
||||
>>> from zero_ex.contract_addresses import network_to_addresses, NetworkId
|
||||
>>> weth_address = network_to_addresses(NetworkId.GANACHE).ether_token
|
||||
>>> zrx_address = network_to_addresses(NetworkId.GANACHE).zrx_token
|
||||
>>> exchange_address = network_to_addresses(NetworkId.GANACHE).exchange
|
||||
|
||||
Wrapping ETH
|
||||
------------
|
||||
@@ -92,15 +92,15 @@ balance:
|
||||
|
||||
>>> from zero_ex.contract_wrappers.erc20_token import ERC20Token
|
||||
>>> zrx_token = ERC20Token(
|
||||
... provider=ganache,
|
||||
... contract_address=NETWORK_TO_ADDRESSES[NetworkId.GANACHE].zrx_token,
|
||||
... web3_or_provider=ganache,
|
||||
... contract_address=network_to_addresses(NetworkId.GANACHE).zrx_token,
|
||||
... )
|
||||
>>> weth_token = ERC20Token(
|
||||
... provider=ganache,
|
||||
... contract_address=NETWORK_TO_ADDRESSES[NetworkId.GANACHE].ether_token,
|
||||
... web3_or_provider=ganache,
|
||||
... contract_address=network_to_addresses(NetworkId.GANACHE).ether_token,
|
||||
... )
|
||||
|
||||
>>> erc20_proxy_addr = NETWORK_TO_ADDRESSES[NetworkId.GANACHE].erc20_proxy
|
||||
>>> erc20_proxy_addr = network_to_addresses(NetworkId.GANACHE).erc20_proxy
|
||||
|
||||
>>> tx = zrx_token.approve.send_transaction(
|
||||
... erc20_proxy_addr,
|
||||
@@ -135,16 +135,20 @@ Constructing an order
|
||||
... takerAssetAmount=to_wei(0.1, 'ether'),
|
||||
... expirationTimeSeconds=round(
|
||||
... (datetime.utcnow() + timedelta(days=1)).timestamp()
|
||||
... )
|
||||
... ),
|
||||
... makerFeeAssetData='0x',
|
||||
... takerFeeAssetData='0x',
|
||||
... )
|
||||
|
||||
For this order to be valid, our Maker must sign a hash of it:
|
||||
|
||||
>>> from zero_ex.order_utils import generate_order_hash_hex
|
||||
>>> order_hash_hex = generate_order_hash_hex(order, exchange_address)
|
||||
>>> order_hash_hex = generate_order_hash_hex(
|
||||
... order, exchange_address, Web3(ganache).eth.chainId
|
||||
... )
|
||||
|
||||
>>> from zero_ex.order_utils import sign_hash_to_bytes
|
||||
>>> maker_signature = sign_hash_to_bytes(
|
||||
>>> from zero_ex.order_utils import sign_hash
|
||||
>>> maker_signature = sign_hash(
|
||||
... ganache, Web3.toChecksumAddress(maker_address), order_hash_hex
|
||||
... )
|
||||
|
||||
@@ -156,16 +160,37 @@ more information on working with Relayers, see `the documentation for
|
||||
Filling an order
|
||||
----------------
|
||||
|
||||
Now our Taker will fill the order. The `takerAssetAmount`:code: parameter
|
||||
specifies the amount of tokens (in this case WETH) that the taker wants to
|
||||
fill. This example fills the order completely, but partial fills are possible
|
||||
too.
|
||||
Now we'll have our Taker fill the order.
|
||||
|
||||
>>> from zero_ex.contract_wrappers.exchange import Exchange
|
||||
>>> exchange = Exchange(
|
||||
... provider=ganache,
|
||||
... contract_address=NETWORK_TO_ADDRESSES[NetworkId.GANACHE].exchange,
|
||||
... web3_or_provider=ganache,
|
||||
... contract_address=network_to_addresses(NetworkId.GANACHE).exchange,
|
||||
... )
|
||||
|
||||
But before filling an order, one may wish to check that it's actually fillable:
|
||||
|
||||
>>> from zero_ex.contract_wrappers.exchange.types import OrderStatus
|
||||
>>> OrderStatus(exchange.get_order_info.call(order)[0])
|
||||
<OrderStatus.FILLABLE: 3>
|
||||
|
||||
The `takerAssetAmount`:code: parameter specifies the amount of tokens (in this
|
||||
case WETH) that the taker wants to fill. This example fills the order
|
||||
completely, but partial fills are possible too.
|
||||
|
||||
One may wish to first call the method in a read-only way, to ensure that it
|
||||
will not revert, and to validate that the return data is as expected:
|
||||
|
||||
>>> exchange.fill_order.call(
|
||||
... order=order,
|
||||
... taker_asset_fill_amount=order["takerAssetAmount"],
|
||||
... signature=maker_signature,
|
||||
... tx_params=TxParams(from_=taker_address)
|
||||
... )
|
||||
(100000000000000000, 100000000000000000, 0, 0, 0)
|
||||
|
||||
Finally, submit the transaction:
|
||||
|
||||
>>> tx_hash = exchange.fill_order.send_transaction(
|
||||
... order=order,
|
||||
... taker_asset_fill_amount=order["takerAssetAmount"],
|
||||
@@ -184,12 +209,15 @@ the exchange wrapper:
|
||||
'makerAddress': '0x...',
|
||||
'makerAssetData': b...,
|
||||
'makerAssetFilledAmount': 100000000000000000,
|
||||
'makerFeeAssetData': b...,
|
||||
'makerFeePaid': 0,
|
||||
'orderHash': b...,
|
||||
'protocolFeePaid': ...,
|
||||
'senderAddress': '0x...',
|
||||
'takerAddress': '0x...',
|
||||
'takerAssetData': b...,
|
||||
'takerAssetFilledAmount': 100000000000000000,
|
||||
'takerFeeAssetData': b...,
|
||||
'takerFeePaid': 0}
|
||||
>>> exchange.get_fill_event(tx_hash)[0].args.takerAssetFilledAmount
|
||||
100000000000000000
|
||||
@@ -206,7 +234,9 @@ A Maker can cancel an order that has yet to be filled.
|
||||
... senderAddress='0x0000000000000000000000000000000000000000',
|
||||
... feeRecipientAddress='0x0000000000000000000000000000000000000000',
|
||||
... makerAssetData=asset_data_utils.encode_erc20(weth_address),
|
||||
... makerFeeAssetData=asset_data_utils.encode_erc20('0x' + '00'*20),
|
||||
... takerAssetData=asset_data_utils.encode_erc20(weth_address),
|
||||
... takerFeeAssetData=asset_data_utils.encode_erc20('0x' + '00'*20),
|
||||
... salt=random.randint(1, 100000000000000000),
|
||||
... makerFee=0,
|
||||
... takerFee=0,
|
||||
@@ -248,7 +278,9 @@ is an example where the taker fills two orders in one transaction:
|
||||
... senderAddress='0x0000000000000000000000000000000000000000',
|
||||
... feeRecipientAddress='0x0000000000000000000000000000000000000000',
|
||||
... makerAssetData=asset_data_utils.encode_erc20(zrx_address),
|
||||
... makerFeeAssetData=asset_data_utils.encode_erc20('0x' + '00'*20),
|
||||
... takerAssetData=asset_data_utils.encode_erc20(weth_address),
|
||||
... takerFeeAssetData=asset_data_utils.encode_erc20('0x' + '00'*20),
|
||||
... salt=random.randint(1, 100000000000000000),
|
||||
... makerFee=0,
|
||||
... takerFee=0,
|
||||
@@ -258,10 +290,12 @@ is an example where the taker fills two orders in one transaction:
|
||||
... (datetime.utcnow() + timedelta(days=1)).timestamp()
|
||||
... )
|
||||
... )
|
||||
>>> signature_1 = sign_hash_to_bytes(
|
||||
>>> signature_1 = sign_hash(
|
||||
... ganache,
|
||||
... Web3.toChecksumAddress(maker_address),
|
||||
... generate_order_hash_hex(order_1, exchange.contract_address)
|
||||
... generate_order_hash_hex(
|
||||
... order_1, exchange.contract_address, Web3(ganache).eth.chainId
|
||||
... ),
|
||||
... )
|
||||
>>> order_2 = Order(
|
||||
... makerAddress=maker_address,
|
||||
@@ -269,7 +303,9 @@ is an example where the taker fills two orders in one transaction:
|
||||
... senderAddress='0x0000000000000000000000000000000000000000',
|
||||
... feeRecipientAddress='0x0000000000000000000000000000000000000000',
|
||||
... makerAssetData=asset_data_utils.encode_erc20(zrx_address),
|
||||
... makerFeeAssetData=asset_data_utils.encode_erc20('0x' + '00'*20),
|
||||
... takerAssetData=asset_data_utils.encode_erc20(weth_address),
|
||||
... takerFeeAssetData=asset_data_utils.encode_erc20('0x' + '00'*20),
|
||||
... salt=random.randint(1, 100000000000000000),
|
||||
... makerFee=0,
|
||||
... takerFee=0,
|
||||
@@ -279,10 +315,12 @@ is an example where the taker fills two orders in one transaction:
|
||||
... (datetime.utcnow() + timedelta(days=1)).timestamp()
|
||||
... )
|
||||
... )
|
||||
>>> signature_2 = sign_hash_to_bytes(
|
||||
>>> signature_2 = sign_hash(
|
||||
... ganache,
|
||||
... Web3.toChecksumAddress(maker_address),
|
||||
... generate_order_hash_hex(order_2, exchange.contract_address)
|
||||
... generate_order_hash_hex(
|
||||
... order_2, exchange.contract_address, Web3(ganache).eth.chainId
|
||||
... ),
|
||||
... )
|
||||
|
||||
Fill order_1 and order_2 together:
|
||||
@@ -308,7 +346,9 @@ will be consumed.
|
||||
... senderAddress='0x0000000000000000000000000000000000000000',
|
||||
... feeRecipientAddress='0x0000000000000000000000000000000000000000',
|
||||
... makerAssetData=asset_data_utils.encode_erc20(weth_address),
|
||||
... makerFeeAssetData=asset_data_utils.encode_erc20('0x' + '00'*20),
|
||||
... takerAssetData=asset_data_utils.encode_erc20(weth_address),
|
||||
... takerFeeAssetData=asset_data_utils.encode_erc20('0x' + '00'*20),
|
||||
... salt=random.randint(1, 100000000000000000),
|
||||
... makerFee=0,
|
||||
... takerFee=0,
|
||||
@@ -320,7 +360,7 @@ will be consumed.
|
||||
... ),
|
||||
... tx_params=TxParams(from_=maker_address),
|
||||
... )
|
||||
73...
|
||||
74...
|
||||
"""
|
||||
|
||||
from .tx_params import TxParams
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Base wrapper class for accessing ethereum smart contracts."""
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, Union
|
||||
|
||||
from eth_utils import is_address, to_checksum_address
|
||||
from web3 import Web3
|
||||
@@ -12,7 +12,11 @@ from .tx_params import TxParams
|
||||
class Validator:
|
||||
"""Base class for validating inputs to methods."""
|
||||
|
||||
def __init__(self, provider: BaseProvider, contract_address: str):
|
||||
def __init__(
|
||||
self,
|
||||
web3_or_provider: Union[Web3, BaseProvider],
|
||||
contract_address: str,
|
||||
):
|
||||
"""Initialize the instance."""
|
||||
|
||||
def assert_valid(
|
||||
@@ -32,7 +36,7 @@ class ContractMethod:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
provider: BaseProvider,
|
||||
web3_or_provider: Union[Web3, BaseProvider],
|
||||
contract_address: str,
|
||||
validator: Validator = None,
|
||||
):
|
||||
@@ -42,9 +46,20 @@ class ContractMethod:
|
||||
:param contract_address: Where the contract has been deployed to.
|
||||
:param validator: Used to validate method inputs.
|
||||
"""
|
||||
self._web3_eth = Web3(provider).eth # pylint: disable=no-member
|
||||
web3 = None
|
||||
if isinstance(web3_or_provider, BaseProvider):
|
||||
web3 = Web3(web3_or_provider)
|
||||
elif isinstance(web3_or_provider, Web3):
|
||||
web3 = web3_or_provider
|
||||
if web3 is None:
|
||||
raise TypeError(
|
||||
"Expected parameter 'web3_or_provider' to be an instance of either"
|
||||
+ " Web3 or BaseProvider"
|
||||
)
|
||||
|
||||
self._web3_eth = web3.eth # pylint: disable=no-member
|
||||
if validator is None:
|
||||
validator = Validator(provider, contract_address)
|
||||
validator = Validator(web3_or_provider, contract_address)
|
||||
self.validator = validator
|
||||
|
||||
@staticmethod
|
||||
@@ -59,8 +74,13 @@ class ContractMethod:
|
||||
if not tx_params:
|
||||
tx_params = TxParams()
|
||||
if not tx_params.from_:
|
||||
tx_params.from_ = (
|
||||
self._web3_eth.defaultAccount or self._web3_eth.accounts[0]
|
||||
tx_params.from_ = self._web3_eth.defaultAccount or (
|
||||
self._web3_eth.accounts[0]
|
||||
if len(self._web3_eth.accounts) > 0
|
||||
else None
|
||||
)
|
||||
if tx_params.from_:
|
||||
tx_params.from_ = self.validate_and_checksum_address(
|
||||
tx_params.from_
|
||||
)
|
||||
tx_params.from_ = self.validate_and_checksum_address(tx_params.from_)
|
||||
return tx_params
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Exception classes common to all wrappers."""
|
||||
|
||||
from inspect import isclass
|
||||
from typing import List
|
||||
|
||||
from eth_abi import decode_abi
|
||||
|
||||
|
||||
class RichRevert(Exception):
|
||||
"""Raised when a contract method returns a rich revert error."""
|
||||
|
||||
def __init__(
|
||||
self, abi_signature: str, param_names: List[str], return_data: str
|
||||
):
|
||||
"""Populate instance variables with decoded return data values."""
|
||||
arg_start_index = abi_signature.index("(") + 1
|
||||
arg_end_index = abi_signature.index(")")
|
||||
arguments = decode_abi(
|
||||
abi_signature[arg_start_index:arg_end_index].split(","),
|
||||
bytes.fromhex(return_data[10:]),
|
||||
)
|
||||
for (param_name, argument) in zip(param_names, arguments):
|
||||
setattr(self, param_name, argument)
|
||||
super().__init__(vars(self))
|
||||
|
||||
|
||||
class NoExceptionForSelector(Exception):
|
||||
"""Indicates that no exception could be found for the given selector."""
|
||||
|
||||
|
||||
def exception_class_from_rich_revert_selector(
|
||||
selector: str, exceptions_module
|
||||
) -> RichRevert:
|
||||
"""Return the appropriate exception class.
|
||||
|
||||
:param selector: A string of the format '0xffffffff' which indicates the
|
||||
4-byte ABI function selector of a rich revert error type, which is
|
||||
expected to be found as a class attribute on some class in
|
||||
`exceptions_module`:code:.
|
||||
:param exceptions_module: The Python module in which to look for a class
|
||||
with a `selector`:code: attribute matching the value of the
|
||||
`selector`:code: argument.
|
||||
"""
|
||||
# noqa: D202 (No blank lines allowed after function docstring
|
||||
def _get_rich_revert_exception_classes():
|
||||
def _exception_name_is_class_with_selector(name: str):
|
||||
if not isclass(getattr(exceptions_module, name)):
|
||||
return False
|
||||
|
||||
try:
|
||||
getattr(exceptions_module, name).selector
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _convert_class_name_to_class(name: str):
|
||||
return getattr(exceptions_module, name)
|
||||
|
||||
return list(
|
||||
map(
|
||||
_convert_class_name_to_class,
|
||||
filter(
|
||||
_exception_name_is_class_with_selector,
|
||||
dir(exceptions_module),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
rich_reverts = _get_rich_revert_exception_classes()
|
||||
|
||||
try:
|
||||
return next(
|
||||
filter(
|
||||
lambda rich_revert: rich_revert.selector == selector,
|
||||
rich_reverts,
|
||||
)
|
||||
)
|
||||
except StopIteration:
|
||||
raise NoExceptionForSelector(selector)
|
||||
@@ -0,0 +1,341 @@
|
||||
"""Exchange-specific exception classes."""
|
||||
|
||||
from enum import auto, Enum
|
||||
|
||||
from zero_ex.contract_wrappers.exceptions import RichRevert
|
||||
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
|
||||
class AssetProxyDispatchErrorCodes(Enum): # noqa: D101 (missing docstring)
|
||||
INVALID_ASSET_DATA_LENGTH = 0
|
||||
UNKNOWN_ASSET_PROXY = auto()
|
||||
|
||||
|
||||
class BatchMatchOrdersErrorCodes(Enum): # noqa: D101 (missing docstring)
|
||||
ZERO_LEFT_ORDERS = 0
|
||||
ZERO_RIGHT_ORDERS = auto()
|
||||
INVALID_LENGTH_LEFT_SIGNATURES = auto()
|
||||
INVALID_LENGTH_RIGHT_SIGNATURES = auto()
|
||||
|
||||
|
||||
class ExchangeContextErrorCodes(Enum): # noqa: D101 (missing docstring)
|
||||
INVALID_MAKER = 0
|
||||
INVALID_TAKER = auto()
|
||||
INVALID_SENDER = auto()
|
||||
|
||||
|
||||
class FillErrorCodes(Enum): # noqa: D101 (missing docstring)
|
||||
INVALID_TAKER_AMOUNT = 0
|
||||
TAKER_OVERPAY = auto()
|
||||
OVERFILL = auto()
|
||||
INVALID_FILL_PRICE = auto()
|
||||
|
||||
|
||||
class SignatureErrorCodes(Enum): # noqa: D101 (missing docstring)
|
||||
BAD_ORDER_SIGNATURE = 0
|
||||
BAD_TRANSACTION_SIGNATURE = auto()
|
||||
INVALID_LENGTH = auto()
|
||||
UNSUPPORTED = auto()
|
||||
ILLEGAL = auto()
|
||||
INAPPROPRIATE_SIGNATURE_TYPE = auto()
|
||||
INVALID_SIGNER = auto()
|
||||
|
||||
|
||||
class TransactionErrorCodes(Enum): # noqa: D101 (missing docstring)
|
||||
ALREADY_EXECUTED = 0
|
||||
EXPIRED = auto()
|
||||
|
||||
|
||||
class IncompleteFillErrorCode(Enum): # noqa: D101 (missing docstring)
|
||||
INCOMPLETE_MARKET_BUY_ORDERS = 0
|
||||
INCOMPLETE_MARKET_SELL_ORDERS = auto()
|
||||
INCOMPLETE_FILL_ORDER = auto()
|
||||
|
||||
|
||||
class SignatureError(RichRevert): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"SignatureError(uint8,bytes32,address,bytes)",
|
||||
["errorCode", "hash", "signerAddress", "signature"],
|
||||
return_data,
|
||||
)
|
||||
|
||||
errorCode: SignatureErrorCodes
|
||||
hash: bytes
|
||||
signerAddress: str
|
||||
signature: bytes
|
||||
|
||||
selector = "0x7e5a2318"
|
||||
|
||||
|
||||
class SignatureValidatorNotApprovedError(
|
||||
RichRevert
|
||||
): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"SignatureValidatorNotApprovedError(address,address)",
|
||||
["signerAddress", "validatorAddress"],
|
||||
return_data,
|
||||
)
|
||||
|
||||
signerAddress: str
|
||||
validatorAddress: str
|
||||
|
||||
selector = "0xa15c0d06"
|
||||
|
||||
|
||||
class EIP1271SignatureError(RichRevert): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"EIP1271SignatureError(address,bytes,bytes,bytes)",
|
||||
["verifyingContractAddress", "data", "signature", "errorData"],
|
||||
return_data,
|
||||
)
|
||||
|
||||
verifyingContractAddress: str
|
||||
data: bytes
|
||||
signature: bytes
|
||||
errorData: bytes
|
||||
|
||||
selector = "0x5bd0428d"
|
||||
|
||||
|
||||
class SignatureWalletError(RichRevert): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"SignatureWalletError(bytes32,address,bytes,bytes)",
|
||||
["hash", "walletAddress", "signature", "errorData"],
|
||||
return_data,
|
||||
)
|
||||
|
||||
hash: bytes
|
||||
walletAddress: str
|
||||
signature: bytes
|
||||
errorData: bytes
|
||||
|
||||
selector = "0x1b8388f7"
|
||||
|
||||
|
||||
class OrderStatusError(RichRevert): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"OrderStatusError(bytes32,uint8)",
|
||||
["orderHash", "orderStatus"],
|
||||
return_data,
|
||||
)
|
||||
|
||||
orderHash: bytes
|
||||
orderStatus: int
|
||||
|
||||
selector = "0xfdb6ca8d"
|
||||
|
||||
|
||||
class ExchangeInvalidContextError(
|
||||
RichRevert
|
||||
): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"ExchangeInvalidContextError(uint8,bytes32,address)",
|
||||
["errorCode", "orderHash", "contextAddress"],
|
||||
return_data,
|
||||
)
|
||||
|
||||
errorCode: ExchangeContextErrorCodes
|
||||
orderHash: bytes
|
||||
contextAddress: str
|
||||
|
||||
selector = "0xe53c76c8"
|
||||
|
||||
|
||||
class FillError(RichRevert): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"FillError(uint8,bytes32)", ["errorCode", "orderHash"], return_data
|
||||
)
|
||||
|
||||
errorCode: FillErrorCodes
|
||||
orderHash: bytes
|
||||
|
||||
selector = "0xe94a7ed0"
|
||||
|
||||
|
||||
class OrderEpochError(RichRevert): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"OrderEpochError(address,address,uint256)",
|
||||
["makerAddress", "orderSenderAddress", "currentEpoch"],
|
||||
return_data,
|
||||
)
|
||||
|
||||
makerAddress: str
|
||||
orderSenderAddress: str
|
||||
currentEpoch: int
|
||||
|
||||
selector = "0x4ad31275"
|
||||
|
||||
|
||||
class AssetProxyExistsError(RichRevert): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"AssetProxyExistsError(bytes4,address)",
|
||||
["assetProxyId", "assetProxyAddress"],
|
||||
return_data,
|
||||
)
|
||||
|
||||
assetProxyId: bytes
|
||||
assetProxyAddress: str
|
||||
|
||||
selector = "0x11c7b720"
|
||||
|
||||
|
||||
class AssetProxyDispatchError(RichRevert): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"AssetProxyDispatchError(uint8,bytes32,bytes)",
|
||||
["errorCode", "orderHash", "assetData"],
|
||||
return_data,
|
||||
)
|
||||
|
||||
errorCode: AssetProxyDispatchErrorCodes
|
||||
orderHash: bytes
|
||||
assetData: bytes
|
||||
|
||||
selector = "0x488219a6"
|
||||
|
||||
|
||||
class AssetProxyTransferError(RichRevert): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"AssetProxyTransferError(bytes32,bytes,bytes)",
|
||||
["orderHash", "assetData", "errorData"],
|
||||
return_data,
|
||||
)
|
||||
|
||||
orderHash: bytes
|
||||
assetData: bytes
|
||||
errorData: bytes
|
||||
|
||||
selector = "0x4678472b"
|
||||
|
||||
|
||||
class NegativeSpreadError(RichRevert): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"NegativeSpreadError(bytes32,bytes32)",
|
||||
["leftOrderHash", "rightOrderHash"],
|
||||
return_data,
|
||||
)
|
||||
|
||||
leftOrderHash: bytes
|
||||
rightOrderHash: bytes
|
||||
|
||||
selector = "0xb6555d6f"
|
||||
|
||||
|
||||
class TransactionError(RichRevert): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"TransactionError(uint8,bytes32)",
|
||||
["errorCode", "transactionHash"],
|
||||
return_data,
|
||||
)
|
||||
|
||||
errorCode: TransactionErrorCodes
|
||||
transactionHash: bytes
|
||||
|
||||
selector = "0xf5985184"
|
||||
|
||||
|
||||
class TransactionExecutionError(RichRevert): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"TransactionExecutionError(bytes32,bytes)",
|
||||
["transactionHash", "errorData"],
|
||||
return_data,
|
||||
)
|
||||
|
||||
transactionHash: bytes
|
||||
errorData: bytes
|
||||
|
||||
selector = "0x20d11f61"
|
||||
|
||||
|
||||
class TransactionGasPriceError(RichRevert): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"TransactionGasPriceError(bytes32,uint256,uint256)",
|
||||
["transactionHash", "actualGasPrice", "requiredGasPrice"],
|
||||
return_data,
|
||||
)
|
||||
|
||||
transactionHash: bytes
|
||||
actualGasPrice: int
|
||||
requiredGasPrice: int
|
||||
|
||||
selector = "0xa26dac09"
|
||||
|
||||
|
||||
class TransactionInvalidContextError(
|
||||
RichRevert
|
||||
): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"TransactionInvalidContextError(bytes32,address)",
|
||||
["transactionHash", "currentContextAddress"],
|
||||
return_data,
|
||||
)
|
||||
|
||||
transactionHash: bytes
|
||||
currentContextAddress: str
|
||||
|
||||
selector = "0xdec4aedf"
|
||||
|
||||
|
||||
class IncompleteFillError(RichRevert): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"IncompleteFillError(uint8,uint256,uint256)",
|
||||
["errorCode", "expectedAssetAmount", "actualAssetAmount"],
|
||||
return_data,
|
||||
)
|
||||
|
||||
errorCode: IncompleteFillErrorCode
|
||||
expectedAssetAmount: int
|
||||
actualAssetAmount: int
|
||||
|
||||
selector = "0x18e4b141"
|
||||
|
||||
|
||||
class BatchMatchOrdersError(RichRevert): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"BatchMatchOrdersError(uint8)", ["errorCode"], return_data
|
||||
)
|
||||
|
||||
errorCode: BatchMatchOrdersErrorCodes
|
||||
|
||||
selector = "0xd4092f4f"
|
||||
|
||||
|
||||
class PayProtocolFeeError(RichRevert): # noqa: D101 (missing docstring)
|
||||
def __init__(self, return_data): # noqa: D107 (missing docstring)
|
||||
super().__init__(
|
||||
"PayProtocolFeeError(bytes32,uint256,address,address,bytes)",
|
||||
[
|
||||
"orderHash",
|
||||
"protocolFee",
|
||||
"makerAddress",
|
||||
"takerAddress",
|
||||
"errorData",
|
||||
],
|
||||
return_data,
|
||||
)
|
||||
|
||||
orderHash: bytes
|
||||
protocolFee: int
|
||||
makerAddress: str
|
||||
takerAddress: str
|
||||
errorData: bytes
|
||||
|
||||
selector = "0x87cb1e75"
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Web3.py-compatible middleware to be injected upon contract instantiation."""
|
||||
|
||||
from zero_ex.contract_wrappers.exceptions import (
|
||||
exception_class_from_rich_revert_selector,
|
||||
NoExceptionForSelector,
|
||||
)
|
||||
|
||||
from . import exceptions
|
||||
|
||||
|
||||
def rich_revert_handler(make_request, _):
|
||||
"""Return a middlware to raise exceptions for rich revert return data."""
|
||||
# noqa: D202 (No blank lines allowed after function docstring
|
||||
def middleware(method, params):
|
||||
response = make_request(method, params)
|
||||
try:
|
||||
raise exception_class_from_rich_revert_selector(
|
||||
response["result"][0:10], exceptions
|
||||
)(response["result"])
|
||||
except NoExceptionForSelector:
|
||||
# response prefix didn't indicate a known error
|
||||
pass
|
||||
except TypeError:
|
||||
# eg "unhashable type: 'slice'". if response["result"] isn't
|
||||
# sliceable (eg if it's a dict), then it definitely isn't a rich
|
||||
# revert.
|
||||
pass
|
||||
except KeyError:
|
||||
# response doesn't have a "result" key
|
||||
pass
|
||||
return response
|
||||
|
||||
return middleware
|
||||
|
||||
|
||||
MIDDLEWARE = [{"layer": 0, "function": rich_revert_handler}]
|
||||
@@ -11,17 +11,13 @@ Converting between the JSON wire format and the types accepted by Web3.py (eg
|
||||
converting Exchange structs between JSON and Python objects.
|
||||
"""
|
||||
|
||||
from copy import copy
|
||||
from typing import cast, Dict
|
||||
|
||||
from eth_utils import remove_0x_prefix
|
||||
|
||||
from zero_ex.json_schemas import assert_valid
|
||||
from enum import auto, Enum
|
||||
|
||||
from . import (
|
||||
Tuple0xbb41e5b3,
|
||||
Tuple0x260219a2,
|
||||
Tuple0x054ca44e,
|
||||
Tuple0x735c43e3,
|
||||
Tuple0x6ca34a6f,
|
||||
Tuple0x4c5ca29b,
|
||||
Tuple0xdabc15fe,
|
||||
Tuple0xb1e4a1ae,
|
||||
)
|
||||
|
||||
@@ -33,27 +29,35 @@ from . import (
|
||||
# of each of these classes.
|
||||
|
||||
|
||||
class FillResults(Tuple0xbb41e5b3):
|
||||
class FillResults(Tuple0x735c43e3):
|
||||
"""The `FillResults`:code: Solidity struct.
|
||||
|
||||
Also known as
|
||||
`zero_ex.contract_wrappers.exchange.Tuple0xbb41e5b3`:py:class:.
|
||||
`zero_ex.contract_wrappers.exchange.Tuple0x735c43e3`:py:class:.
|
||||
"""
|
||||
|
||||
|
||||
class Order(Tuple0x260219a2):
|
||||
class Order(Tuple0x6ca34a6f):
|
||||
"""The `Order`:code: Solidity struct.
|
||||
|
||||
Also known as
|
||||
`zero_ex.contract_wrappers.exchange.Tuple0x260219a2`:py:class:.
|
||||
`zero_ex.contract_wrappers.exchange.Tuple0x6ca34a6f`:py:class:.
|
||||
"""
|
||||
|
||||
|
||||
class MatchedFillResults(Tuple0x054ca44e):
|
||||
class MatchedFillResults(Tuple0x4c5ca29b):
|
||||
"""The `MatchedFillResults`:code: Solidity struct.
|
||||
|
||||
Also known as
|
||||
`zero_ex.contract_wrappers.exchange.Tuple0x054ca44e`:py:class:.
|
||||
`zero_ex.contract_wrappers.exchange.Tuple0x4c5ca29b`:py:class:.
|
||||
"""
|
||||
|
||||
|
||||
class ZeroExTransaction(Tuple0xdabc15fe):
|
||||
"""The `ZeroExTransaction`:code: Solidity struct.
|
||||
|
||||
Also known as
|
||||
`zero_ex.contract_wrappers.exchange.Tuple0xdabc15fe`:py:class:.
|
||||
"""
|
||||
|
||||
|
||||
@@ -65,136 +69,11 @@ class OrderInfo(Tuple0xb1e4a1ae):
|
||||
"""
|
||||
|
||||
|
||||
def order_to_jsdict(
|
||||
order: Order,
|
||||
exchange_address="0x0000000000000000000000000000000000000000",
|
||||
signature: str = None,
|
||||
) -> dict:
|
||||
"""Convert a Web3-compatible order struct to a JSON-schema-compatible dict.
|
||||
|
||||
More specifically, do explicit decoding for the `bytes`:code: fields, and
|
||||
convert numerics to strings.
|
||||
|
||||
>>> import pprint
|
||||
>>> pprint.pprint(order_to_jsdict(
|
||||
... {
|
||||
... 'makerAddress': "0x0000000000000000000000000000000000000000",
|
||||
... 'takerAddress': "0x0000000000000000000000000000000000000000",
|
||||
... 'feeRecipientAddress':
|
||||
... "0x0000000000000000000000000000000000000000",
|
||||
... 'senderAddress': "0x0000000000000000000000000000000000000000",
|
||||
... 'makerAssetAmount': 1,
|
||||
... 'takerAssetAmount': 1,
|
||||
... 'makerFee': 0,
|
||||
... 'takerFee': 0,
|
||||
... 'expirationTimeSeconds': 1,
|
||||
... 'salt': 1,
|
||||
... 'makerAssetData': (0).to_bytes(1, byteorder='big') * 20,
|
||||
... 'takerAssetData': (0).to_bytes(1, byteorder='big') * 20,
|
||||
... },
|
||||
... ))
|
||||
{'exchangeAddress': '0x0000000000000000000000000000000000000000',
|
||||
'expirationTimeSeconds': '1',
|
||||
'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
|
||||
'makerAddress': '0x0000000000000000000000000000000000000000',
|
||||
'makerAssetAmount': '1',
|
||||
'makerAssetData': '0x0000000000000000000000000000000000000000',
|
||||
'makerFee': '0',
|
||||
'salt': '1',
|
||||
'senderAddress': '0x0000000000000000000000000000000000000000',
|
||||
'takerAddress': '0x0000000000000000000000000000000000000000',
|
||||
'takerAssetAmount': '1',
|
||||
'takerAssetData': '0x0000000000000000000000000000000000000000',
|
||||
'takerFee': '0'}
|
||||
"""
|
||||
jsdict = cast(Dict, copy(order))
|
||||
|
||||
# encode bytes fields
|
||||
jsdict["makerAssetData"] = "0x" + order["makerAssetData"].hex()
|
||||
jsdict["takerAssetData"] = "0x" + order["takerAssetData"].hex()
|
||||
|
||||
jsdict["exchangeAddress"] = exchange_address
|
||||
|
||||
jsdict["expirationTimeSeconds"] = str(order["expirationTimeSeconds"])
|
||||
|
||||
jsdict["makerAssetAmount"] = str(order["makerAssetAmount"])
|
||||
jsdict["takerAssetAmount"] = str(order["takerAssetAmount"])
|
||||
|
||||
jsdict["makerFee"] = str(order["makerFee"])
|
||||
jsdict["takerFee"] = str(order["takerFee"])
|
||||
|
||||
jsdict["salt"] = str(order["salt"])
|
||||
|
||||
if signature is not None:
|
||||
jsdict["signature"] = signature
|
||||
|
||||
assert_valid(jsdict, "/orderSchema")
|
||||
|
||||
return jsdict
|
||||
|
||||
|
||||
def jsdict_to_order(jsdict: dict) -> Order:
|
||||
r"""Convert a JSON-schema-compatible dict order to a Web3-compatible struct.
|
||||
|
||||
More specifically, do explicit encoding of the `bytes`:code: fields, and
|
||||
parse integers from strings.
|
||||
|
||||
>>> import pprint
|
||||
>>> pprint.pprint(jsdict_to_order(
|
||||
... {
|
||||
... 'makerAddress': "0x0000000000000000000000000000000000000000",
|
||||
... 'takerAddress': "0x0000000000000000000000000000000000000000",
|
||||
... 'feeRecipientAddress': "0x0000000000000000000000000000000000000000",
|
||||
... 'senderAddress': "0x0000000000000000000000000000000000000000",
|
||||
... 'makerAssetAmount': "1000000000000000000",
|
||||
... 'takerAssetAmount': "1000000000000000000",
|
||||
... 'makerFee': "0",
|
||||
... 'takerFee': "0",
|
||||
... 'expirationTimeSeconds': "12345",
|
||||
... 'salt': "12345",
|
||||
... 'makerAssetData': "0x0000000000000000000000000000000000000000",
|
||||
... 'takerAssetData': "0x0000000000000000000000000000000000000000",
|
||||
... 'exchangeAddress': "0x0000000000000000000000000000000000000000",
|
||||
... },
|
||||
... ))
|
||||
{'expirationTimeSeconds': 12345,
|
||||
'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
|
||||
'makerAddress': '0x0000000000000000000000000000000000000000',
|
||||
'makerAssetAmount': 1000000000000000000,
|
||||
'makerAssetData': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
'makerFee': 0,
|
||||
'salt': 12345,
|
||||
'senderAddress': '0x0000000000000000000000000000000000000000',
|
||||
'takerAddress': '0x0000000000000000000000000000000000000000',
|
||||
'takerAssetAmount': 1000000000000000000,
|
||||
'takerAssetData': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
'takerFee': 0}
|
||||
""" # noqa: E501 (line too long)
|
||||
assert_valid(jsdict, "/orderSchema")
|
||||
|
||||
order = cast(Order, copy(jsdict))
|
||||
|
||||
order["makerAssetData"] = bytes.fromhex(
|
||||
remove_0x_prefix(jsdict["makerAssetData"])
|
||||
)
|
||||
order["takerAssetData"] = bytes.fromhex(
|
||||
remove_0x_prefix(jsdict["takerAssetData"])
|
||||
)
|
||||
|
||||
order["makerAssetAmount"] = int(jsdict["makerAssetAmount"])
|
||||
order["takerAssetAmount"] = int(jsdict["takerAssetAmount"])
|
||||
|
||||
order["makerFee"] = int(jsdict["makerFee"])
|
||||
order["takerFee"] = int(jsdict["takerFee"])
|
||||
|
||||
order["expirationTimeSeconds"] = int(jsdict["expirationTimeSeconds"])
|
||||
|
||||
order["salt"] = int(jsdict["salt"])
|
||||
|
||||
del order["exchangeAddress"] # type: ignore
|
||||
# silence mypy pending release of
|
||||
# https://github.com/python/mypy/issues/3550
|
||||
|
||||
return order
|
||||
class OrderStatus(Enum): # noqa: D101 # pylint: disable=missing-docstring
|
||||
INVALID = 0
|
||||
INVALID_MAKER_ASSET_AMOUNT = auto()
|
||||
INVALID_TAKER_ASSET_AMOUNT = auto()
|
||||
FILLABLE = auto()
|
||||
EXPIRED = auto()
|
||||
FULLY_FILLED = auto()
|
||||
CANCELLED = auto()
|
||||
|
||||
@@ -1,22 +1,40 @@
|
||||
"""Validate inputs to the Exchange contract."""
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, Union
|
||||
|
||||
from web3 import Web3
|
||||
from web3.providers.base import BaseProvider
|
||||
|
||||
from zero_ex import json_schemas
|
||||
from zero_ex.contract_wrappers.order_conversions import order_to_jsdict
|
||||
|
||||
from ..bases import Validator
|
||||
from .types import order_to_jsdict
|
||||
|
||||
|
||||
class ExchangeValidator(Validator):
|
||||
"""Validate inputs to Exchange methods."""
|
||||
|
||||
def __init__(self, provider: BaseProvider, contract_address: str):
|
||||
def __init__(
|
||||
self,
|
||||
web3_or_provider: Union[Web3, BaseProvider],
|
||||
contract_address: str,
|
||||
):
|
||||
"""Initialize the class."""
|
||||
super().__init__(provider, contract_address)
|
||||
super().__init__(web3_or_provider, contract_address)
|
||||
|
||||
web3 = None
|
||||
if isinstance(web3_or_provider, BaseProvider):
|
||||
web3 = Web3(web3_or_provider)
|
||||
elif isinstance(web3_or_provider, Web3):
|
||||
web3 = web3_or_provider
|
||||
if web3 is None:
|
||||
raise TypeError(
|
||||
"Expected parameter 'web3_or_provider' to be an instance of either"
|
||||
+ " Web3 or BaseProvider"
|
||||
)
|
||||
|
||||
self.contract_address = contract_address
|
||||
self.chain_id = web3.eth.chainId
|
||||
|
||||
def assert_valid(
|
||||
self, method_name: str, parameter_name: str, argument_value: Any
|
||||
@@ -30,13 +48,17 @@ class ExchangeValidator(Validator):
|
||||
"""
|
||||
if parameter_name == "order":
|
||||
json_schemas.assert_valid(
|
||||
order_to_jsdict(argument_value, self.contract_address),
|
||||
order_to_jsdict(
|
||||
argument_value, self.chain_id, self.contract_address
|
||||
),
|
||||
"/orderSchema",
|
||||
)
|
||||
|
||||
if parameter_name == "orders":
|
||||
for order in argument_value:
|
||||
json_schemas.assert_valid(
|
||||
order_to_jsdict(order, self.contract_address),
|
||||
order_to_jsdict(
|
||||
order, self.chain_id, self.contract_address
|
||||
),
|
||||
"/orderSchema",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
"""Utilities to convert between JSON and Python-native objects."""
|
||||
|
||||
from copy import copy
|
||||
from typing import cast, Dict, Union
|
||||
|
||||
from eth_utils import remove_0x_prefix
|
||||
|
||||
from zero_ex.json_schemas import assert_valid
|
||||
from zero_ex.contract_wrappers.exchange.types import Order
|
||||
|
||||
|
||||
def order_to_jsdict(
|
||||
order: Order,
|
||||
chain_id: int,
|
||||
exchange_address="0x0000000000000000000000000000000000000000",
|
||||
signature: str = None,
|
||||
) -> dict:
|
||||
"""Convert a Web3-compatible order struct to a JSON-schema-compatible dict.
|
||||
|
||||
More specifically, do explicit decoding for the `bytes`:code: fields, and
|
||||
convert numerics to strings.
|
||||
|
||||
>>> import pprint
|
||||
>>> pprint.pprint(order_to_jsdict(
|
||||
... {
|
||||
... 'makerAddress': "0x0000000000000000000000000000000000000000",
|
||||
... 'takerAddress': "0x0000000000000000000000000000000000000000",
|
||||
... 'feeRecipientAddress':
|
||||
... "0x0000000000000000000000000000000000000000",
|
||||
... 'senderAddress': "0x0000000000000000000000000000000000000000",
|
||||
... 'makerAssetAmount': 1,
|
||||
... 'takerAssetAmount': 1,
|
||||
... 'makerFee': 0,
|
||||
... 'takerFee': 0,
|
||||
... 'expirationTimeSeconds': 1,
|
||||
... 'salt': 1,
|
||||
... 'makerAssetData': (0).to_bytes(1, byteorder='big') * 20,
|
||||
... 'takerAssetData': (0).to_bytes(1, byteorder='big') * 20,
|
||||
... 'makerFeeAssetData': (0).to_bytes(1, byteorder='big') * 20,
|
||||
... 'takerFeeAssetData': (0).to_bytes(1, byteorder='big') * 20,
|
||||
... },
|
||||
... chain_id=50
|
||||
... ))
|
||||
{'chainId': 50,
|
||||
'exchangeAddress': '0x0000000000000000000000000000000000000000',
|
||||
'expirationTimeSeconds': '1',
|
||||
'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
|
||||
'makerAddress': '0x0000000000000000000000000000000000000000',
|
||||
'makerAssetAmount': '1',
|
||||
'makerAssetData': '0x0000000000000000000000000000000000000000',
|
||||
'makerFee': '0',
|
||||
'makerFeeAssetData': '0x0000000000000000000000000000000000000000',
|
||||
'salt': '1',
|
||||
'senderAddress': '0x0000000000000000000000000000000000000000',
|
||||
'takerAddress': '0x0000000000000000000000000000000000000000',
|
||||
'takerAssetAmount': '1',
|
||||
'takerAssetData': '0x0000000000000000000000000000000000000000',
|
||||
'takerFee': '0',
|
||||
'takerFeeAssetData': '0x0000000000000000000000000000000000000000'}
|
||||
"""
|
||||
jsdict = cast(Dict, copy(order))
|
||||
|
||||
def encode_bytes(bytes_or_str: Union[bytes, str]) -> bytes:
|
||||
def ensure_hex_prefix(hex_str: str):
|
||||
if hex_str[0:2] != "0x":
|
||||
hex_str = "0x" + hex_str
|
||||
return hex_str
|
||||
|
||||
return ensure_hex_prefix(
|
||||
cast(bytes, bytes_or_str).hex()
|
||||
if isinstance(bytes_or_str, bytes)
|
||||
else bytes_or_str
|
||||
)
|
||||
|
||||
jsdict["makerAssetData"] = encode_bytes(order["makerAssetData"])
|
||||
jsdict["takerAssetData"] = encode_bytes(order["takerAssetData"])
|
||||
jsdict["makerFeeAssetData"] = encode_bytes(order["makerFeeAssetData"])
|
||||
jsdict["takerFeeAssetData"] = encode_bytes(order["takerFeeAssetData"])
|
||||
|
||||
jsdict["exchangeAddress"] = exchange_address
|
||||
|
||||
jsdict["expirationTimeSeconds"] = str(order["expirationTimeSeconds"])
|
||||
|
||||
jsdict["makerAssetAmount"] = str(order["makerAssetAmount"])
|
||||
jsdict["takerAssetAmount"] = str(order["takerAssetAmount"])
|
||||
|
||||
jsdict["makerFee"] = str(order["makerFee"])
|
||||
jsdict["takerFee"] = str(order["takerFee"])
|
||||
|
||||
jsdict["salt"] = str(order["salt"])
|
||||
|
||||
jsdict["chainId"] = chain_id
|
||||
|
||||
if signature is not None:
|
||||
jsdict["signature"] = signature
|
||||
|
||||
assert_valid(jsdict, "/orderSchema")
|
||||
|
||||
return jsdict
|
||||
|
||||
|
||||
def jsdict_to_order(jsdict: dict) -> Order:
|
||||
r"""Convert a JSON-schema-compatible dict order to a Web3-compatible struct.
|
||||
|
||||
More specifically, do explicit encoding of the `bytes`:code: fields, and
|
||||
parse integers from strings.
|
||||
|
||||
>>> import pprint
|
||||
>>> pprint.pprint(jsdict_to_order(
|
||||
... {
|
||||
... 'makerAddress': "0x0000000000000000000000000000000000000000",
|
||||
... 'takerAddress': "0x0000000000000000000000000000000000000000",
|
||||
... 'feeRecipientAddress': "0x0000000000000000000000000000000000000000",
|
||||
... 'senderAddress': "0x0000000000000000000000000000000000000000",
|
||||
... 'makerAssetAmount': "1000000000000000000",
|
||||
... 'takerAssetAmount': "1000000000000000000",
|
||||
... 'makerFee': "0",
|
||||
... 'takerFee': "0",
|
||||
... 'expirationTimeSeconds': "12345",
|
||||
... 'salt': "12345",
|
||||
... 'makerAssetData': "0x0000000000000000000000000000000000000000",
|
||||
... 'takerAssetData': "0x0000000000000000000000000000000000000000",
|
||||
... 'makerFeeAssetData': "0x0000000000000000000000000000000000000000",
|
||||
... 'takerFeeAssetData': "0x0000000000000000000000000000000000000000",
|
||||
... 'exchangeAddress': "0x0000000000000000000000000000000000000000",
|
||||
... 'chainId': 50
|
||||
... },
|
||||
... ))
|
||||
{'chainId': 50,
|
||||
'expirationTimeSeconds': 12345,
|
||||
'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
|
||||
'makerAddress': '0x0000000000000000000000000000000000000000',
|
||||
'makerAssetAmount': 1000000000000000000,
|
||||
'makerAssetData': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
'makerFee': 0,
|
||||
'makerFeeAssetData': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
'salt': 12345,
|
||||
'senderAddress': '0x0000000000000000000000000000000000000000',
|
||||
'takerAddress': '0x0000000000000000000000000000000000000000',
|
||||
'takerAssetAmount': 1000000000000000000,
|
||||
'takerAssetData': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
'takerFee': 0,
|
||||
'takerFeeAssetData': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
b'\x00\x00\x00\x00\x00\x00\x00\x00'}
|
||||
""" # noqa: E501 (line too long)
|
||||
assert_valid(jsdict, "/orderSchema")
|
||||
|
||||
order = cast(Order, copy(jsdict))
|
||||
|
||||
order["makerAssetData"] = bytes.fromhex(
|
||||
remove_0x_prefix(jsdict["makerAssetData"])
|
||||
)
|
||||
order["makerFeeAssetData"] = bytes.fromhex(
|
||||
remove_0x_prefix(jsdict["makerFeeAssetData"])
|
||||
)
|
||||
order["takerAssetData"] = bytes.fromhex(
|
||||
remove_0x_prefix(jsdict["takerAssetData"])
|
||||
)
|
||||
order["takerFeeAssetData"] = bytes.fromhex(
|
||||
remove_0x_prefix(jsdict["takerFeeAssetData"])
|
||||
)
|
||||
|
||||
order["makerAssetAmount"] = int(jsdict["makerAssetAmount"])
|
||||
order["takerAssetAmount"] = int(jsdict["takerAssetAmount"])
|
||||
|
||||
order["makerFee"] = int(jsdict["makerFee"])
|
||||
order["takerFee"] = int(jsdict["takerFee"])
|
||||
|
||||
order["expirationTimeSeconds"] = int(jsdict["expirationTimeSeconds"])
|
||||
|
||||
order["salt"] = int(jsdict["salt"])
|
||||
|
||||
del order["exchangeAddress"] # type: ignore
|
||||
# silence mypy pending release of
|
||||
# https://github.com/python/mypy/issues/3550
|
||||
|
||||
return order
|
||||
@@ -23,7 +23,7 @@ class TxParams:
|
||||
gas: Optional[int] = attr.ib(
|
||||
default=None, converter=attr.converters.optional(int)
|
||||
)
|
||||
gasPrice: Optional[int] = attr.ib(
|
||||
gas_price: Optional[int] = attr.ib(
|
||||
default=None, converter=attr.converters.optional(int)
|
||||
)
|
||||
nonce: Optional[int] = attr.ib(
|
||||
@@ -36,4 +36,7 @@ class TxParams:
|
||||
if "from_" in res:
|
||||
res["from"] = res["from_"]
|
||||
del res["from_"]
|
||||
if "gas_price" in res:
|
||||
res["gasPrice"] = res["gas_price"]
|
||||
del res["gas_price"]
|
||||
return res
|
||||
|
||||
@@ -26,16 +26,24 @@ class Web3:
|
||||
class middleware_stack:
|
||||
@staticmethod
|
||||
def get(key: str) -> Callable: ...
|
||||
|
||||
def inject(
|
||||
self, middleware_func: object, layer: object
|
||||
) -> None: ...
|
||||
|
||||
...
|
||||
|
||||
middleware_onion: middleware_stack
|
||||
|
||||
class net:
|
||||
version: str
|
||||
...
|
||||
|
||||
|
||||
class eth:
|
||||
class Eth:
|
||||
defaultAccount: str
|
||||
accounts: List[str]
|
||||
chainId: int
|
||||
...
|
||||
|
||||
class account:
|
||||
@@ -53,4 +61,7 @@ class Web3:
|
||||
@staticmethod
|
||||
def isAddress(address: str) -> bool: ...
|
||||
...
|
||||
|
||||
eth: Eth
|
||||
|
||||
...
|
||||
|
||||
@@ -5,7 +5,7 @@ from eth_utils import to_checksum_address
|
||||
from web3 import Web3
|
||||
|
||||
from zero_ex.order_utils import asset_data_utils
|
||||
from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES, NetworkId
|
||||
from zero_ex.contract_addresses import network_to_addresses, NetworkId
|
||||
from zero_ex.contract_artifacts import abi_by_name
|
||||
|
||||
|
||||
@@ -36,14 +36,14 @@ def accounts(web3_eth): # pylint: disable=redefined-outer-name
|
||||
@pytest.fixture(scope="module")
|
||||
def erc20_proxy_address():
|
||||
"""Get the 0x ERC20 Proxy address."""
|
||||
return NETWORK_TO_ADDRESSES[NetworkId.GANACHE].erc20_proxy
|
||||
return network_to_addresses(NetworkId.GANACHE).erc20_proxy
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def weth_asset_data(): # pylint: disable=redefined-outer-name
|
||||
"""Get 0x asset data for Wrapped Ether (WETH) token."""
|
||||
return asset_data_utils.encode_erc20(
|
||||
NETWORK_TO_ADDRESSES[NetworkId.GANACHE].ether_token
|
||||
network_to_addresses(NetworkId.GANACHE).ether_token
|
||||
)
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ def weth_instance(web3_eth): # pylint: disable=redefined-outer-name
|
||||
"""Get an instance of the WrapperEther contract."""
|
||||
return web3_eth.contract(
|
||||
address=to_checksum_address(
|
||||
NETWORK_TO_ADDRESSES[NetworkId.GANACHE].ether_token
|
||||
network_to_addresses(NetworkId.GANACHE).ether_token
|
||||
),
|
||||
abi=abi_by_name("WETH9"),
|
||||
)
|
||||
@@ -61,7 +61,7 @@ def weth_instance(web3_eth): # pylint: disable=redefined-outer-name
|
||||
@pytest.fixture(scope="module")
|
||||
def zrx_address():
|
||||
"""Get address of ZRX token for Ganache network."""
|
||||
return NETWORK_TO_ADDRESSES[NetworkId.GANACHE].zrx_token
|
||||
return network_to_addresses(NetworkId.GANACHE).zrx_token
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES, NetworkId
|
||||
from zero_ex.contract_addresses import network_to_addresses, NetworkId
|
||||
from zero_ex.contract_wrappers.bases import ContractMethod
|
||||
|
||||
|
||||
@@ -10,6 +10,6 @@ from zero_ex.contract_wrappers.bases import ContractMethod
|
||||
def contract_wrapper(ganache_provider):
|
||||
"""Get a ContractMethod instance for testing."""
|
||||
return ContractMethod(
|
||||
provider=ganache_provider,
|
||||
contract_address=NETWORK_TO_ADDRESSES[NetworkId.GANACHE].ether_token,
|
||||
web3_or_provider=ganache_provider,
|
||||
contract_address=network_to_addresses(NetworkId.GANACHE).ether_token,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES, NetworkId
|
||||
from zero_ex.contract_addresses import network_to_addresses, NetworkId
|
||||
from zero_ex.contract_wrappers import TxParams
|
||||
from zero_ex.contract_wrappers.erc20_token import ERC20Token
|
||||
|
||||
@@ -16,7 +16,7 @@ MAX_ALLOWANCE = int("{:.0f}".format(Decimal(2) ** 256 - 1))
|
||||
def erc20_wrapper(ganache_provider):
|
||||
"""Get an instance of ERC20Token wrapper class for testing."""
|
||||
return ERC20Token(
|
||||
ganache_provider, NETWORK_TO_ADDRESSES[NetworkId.GANACHE].ether_token
|
||||
ganache_provider, network_to_addresses(NetworkId.GANACHE).ether_token
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -5,20 +5,24 @@ import random
|
||||
import pytest
|
||||
from eth_utils import remove_0x_prefix
|
||||
|
||||
from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES, NetworkId
|
||||
from zero_ex.contract_addresses import network_to_addresses, NetworkId
|
||||
from zero_ex.contract_wrappers import TxParams
|
||||
from zero_ex.contract_wrappers.exchange import Exchange
|
||||
from zero_ex.contract_wrappers.exchange.types import Order
|
||||
from zero_ex.json_schemas import assert_valid
|
||||
from zero_ex.order_utils import generate_order_hash_hex, sign_hash_to_bytes
|
||||
from zero_ex.order_utils import (
|
||||
asset_data_utils,
|
||||
generate_order_hash_hex,
|
||||
sign_hash,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def exchange_wrapper(ganache_provider):
|
||||
"""Get an Exchange wrapper instance."""
|
||||
return Exchange(
|
||||
provider=ganache_provider,
|
||||
contract_address=NETWORK_TO_ADDRESSES[NetworkId.GANACHE].exchange,
|
||||
web3_or_provider=ganache_provider,
|
||||
contract_address=network_to_addresses(NetworkId.GANACHE).exchange,
|
||||
)
|
||||
|
||||
|
||||
@@ -43,6 +47,8 @@ def create_test_order(
|
||||
salt=random.randint(1, 1000000000),
|
||||
makerAssetData=maker_asset_data,
|
||||
takerAssetData=taker_asset_data,
|
||||
makerFeeAssetData=asset_data_utils.encode_erc20("0x" + "00" * 20),
|
||||
takerFeeAssetData=asset_data_utils.encode_erc20("0x" + "00" * 20),
|
||||
)
|
||||
return order
|
||||
|
||||
@@ -67,16 +73,29 @@ def test_exchange_wrapper__fill_order(
|
||||
exchange_wrapper, # pylint: disable=redefined-outer-name
|
||||
ganache_provider,
|
||||
weth_asset_data,
|
||||
zrx_asset_data,
|
||||
):
|
||||
"""Test filling an order."""
|
||||
taker = accounts[0]
|
||||
maker = accounts[1]
|
||||
exchange_address = exchange_wrapper.contract_address
|
||||
order = create_test_order(maker, 1, weth_asset_data, 1, weth_asset_data)
|
||||
order = create_test_order(maker, 1, weth_asset_data, 1, zrx_asset_data)
|
||||
order_hash = generate_order_hash_hex(
|
||||
order=order, exchange_address=exchange_address
|
||||
order=order, exchange_address=exchange_address, chain_id=1337
|
||||
)
|
||||
order_signature = sign_hash_to_bytes(ganache_provider, maker, order_hash)
|
||||
order_signature = sign_hash(ganache_provider, maker, order_hash)
|
||||
|
||||
fill_results = exchange_wrapper.fill_order.call(
|
||||
order=order,
|
||||
taker_asset_fill_amount=order["takerAssetAmount"],
|
||||
signature=order_signature,
|
||||
tx_params=TxParams(from_=taker),
|
||||
)
|
||||
assert fill_results[0] == 1
|
||||
assert fill_results[1] == 1
|
||||
assert fill_results[2] == 0
|
||||
assert fill_results[3] == 0
|
||||
assert fill_results[4] == 0
|
||||
|
||||
tx_hash = exchange_wrapper.fill_order.send_transaction(
|
||||
order=order,
|
||||
@@ -107,11 +126,13 @@ def test_exchange_wrapper__batch_fill_orders(
|
||||
orders.append(order_1)
|
||||
orders.append(order_2)
|
||||
order_hashes = [
|
||||
generate_order_hash_hex(order=order, exchange_address=exchange_address)
|
||||
generate_order_hash_hex(
|
||||
order=order, exchange_address=exchange_address, chain_id=1337
|
||||
)
|
||||
for order in orders
|
||||
]
|
||||
order_signatures = [
|
||||
sign_hash_to_bytes(ganache_provider, maker, order_hash)
|
||||
sign_hash(ganache_provider, maker, order_hash)
|
||||
for order_hash in order_hashes
|
||||
]
|
||||
taker_amounts = [order["takerAssetAmount"] for order in orders]
|
||||
@@ -128,3 +149,20 @@ def test_exchange_wrapper__batch_fill_orders(
|
||||
assert_fill_log(
|
||||
fill_events[index].args, maker, taker, order, order_hashes[index]
|
||||
)
|
||||
|
||||
|
||||
def test_two_instantiations_with_web3_objects(web3_instance):
|
||||
"""Test that instantiating two Exchange objects doesn't raise.
|
||||
|
||||
When instantiating an Exchange object with a web3 client (rather than a
|
||||
provider) there was a bug encountered where web3.py was giving an error
|
||||
when trying to install the rich-revert-handling middleware on the web3
|
||||
client, an error saying "can't install this same middleware instance
|
||||
again." Test that that bug isn't occurring.
|
||||
"""
|
||||
exchange = Exchange( # pylint: disable=unused-variable
|
||||
web3_instance, network_to_addresses(NetworkId.GANACHE).exchange
|
||||
)
|
||||
exchange2 = Exchange( # pylint: disable=unused-variable
|
||||
web3_instance, network_to_addresses(NetworkId.GANACHE).exchange
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ commands =
|
||||
pytest test
|
||||
|
||||
[testenv:run_tests_against_deployment]
|
||||
setenv = PY_IGNORE_IMPORTMISMATCH = 1
|
||||
commands =
|
||||
pip install 0x-contract-wrappers
|
||||
pytest test
|
||||
pip install 0x-contract-wrappers[dev]
|
||||
pytest --doctest-modules src test
|
||||
|
||||
Reference in New Issue
Block a user