Python nested wrapper methods & estimate_gas (#1996)

* git rm unnecessary .gitkeep file

* After all Pytest runs, show short test summary

* abi-gen/Py: facilitate inlining of parameter lists

Effectively, stopped new-lines from being introduced by calls to the
`params` and `typed_params` partials.

* abi-gen: simple Py wrapper test for local dev'ment

* abi-gen/Py: stop gen'ing ValidatorBase

* abi-gen/Py: declare abi() wrapper method in Base

* abi-gen/Py: methods as classes to ease call/sendTx

Represent methods as classes in order to faciliate access to a method's
different operations (call, send_transaction, etc).

* contract_wrappers.py: make Base methods public

Changed some methods on BaseContractWrapper to be public.

* contract_wrappers.py: remove unused method

* contract_wrappers.py: extract method

* abi-gen/Py: inline method

* contract_wrappers.py: fix bug in call()

We were passing transaction parameters through to sendTransaction()
invocations, but not to call() invocations.

* abi-gen/Py: remove `view_only` param to call/tx

Formerly, in the BaseContractWrapper, there was just one function used
for both eth_call and eth_sendTransaction, and you would distinguish
between the two by specifying `view_only=True` when you wanted a call.

This commit defines a method dedicated to executing an eth_call, and
leaves the old method behind, with the `view_only` param removed, to be
used for eth_sendTransaction.

* abi-gen/Py: rename method

* contract_wrappers/Py: simplify web3 func handling

Pass web3 function instance into generated wrapper method class
constructor, rather than having that class obtain it upon each method
call.

Really this is just an elimination of a call to
BaseContractWrapper.contract_instance(), which will be removed
completely in a shortly-upcoming commit.

* contract_wrappers.py: inline method

Inline and remove method BaseContractWrapper.contract_instance().

* contract_wrappers.py: pass Validator to *Method

Pass a ValidatorBase instance into construction of the contract method
classes, *Method, to eliminate another dependency on the containing
contract object, which will be eliminated completely in a
shortly-upcoming commit.

* abi-gen/Py: BaseContractWrapper -> ContractMethod

Change the fundamental thing-to-be-wrapped from the contract to the
method.  Since the named method classes were introduced (in a previous
commit), and since the operations contained within the Base are
predominantly focused on supporting method calls more than anything
else, it makes more intuitive sense to provide a base for the methods
than for the contract.

With this change, the method classes no longer require a contract object
to be passed to their constructors.  The contract members that the
methods were utilizing are now passed directly to the method
constructor.

* contract_wrappers.py: rename module to bases...

...from _base_contract_wrapper.  The old name hasn't made sense since
ValidatorBase was moved into that module, and definitely doesn't make
sense now that the fundamental thing-to-be-wrapped has changed from the
contract to the method.  Also renamed to make it public (removed the
leading underscore) since we're generating code that will depend on it.

* abi-gen/Py: clarify call/sendTx docstrings

* abi-gen/Py: adjust whitespace

* contract_wrappers.py: inline method

* abi-gen/Py: rename class ValidatorBase...

...to just Validator.  It's in the "bases" module, which provides the
context needed in order to know it's a base class

* python-packages: fix silent failures of ./parallel

* contract_wrappers.py: remove private_key support

Having this present was overcomplicating interfaces.  And it was
untested (and not readily working when testing was attempted).  And it
only provided a thin layer of convenience, which a client could easily
code up themselves.

* contract_wrappers.py: inline method

* contract_wrappers.py: rm unused member variables

* contract_wrappers.py: rm unnecessary instance var

* abi-gen/Py: add estimate_gas to gen'd methods

* update CHANGELOG.json
This commit is contained in:
F. Eugene Aumson 2019-08-01 12:47:52 -04:00 committed by GitHub
parent 4eb0767834
commit 57318c0041
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1070 additions and 865 deletions

View File

@ -11,37 +11,18 @@ from typing import ( # pylint: disable=unused-import
Union,
)
from eth_utils import to_checksum_address
from mypy_extensions import TypedDict # pylint: disable=unused-import
from hexbytes import HexBytes
from web3 import Web3
from web3.contract import ContractFunction
from web3.datastructures import AttributeDict
from web3.providers.base import BaseProvider
from zero_ex.contract_wrappers._base_contract_wrapper import BaseContractWrapper
from zero_ex.contract_wrappers.bases import ContractMethod, Validator
from zero_ex.contract_wrappers.tx_params import TxParams
class {{contractName}}ValidatorBase:
"""Base class for validating inputs to {{contractName}} methods."""
def __init__(
self,
provider: BaseProvider,
contract_address: str,
private_key: str = None,
):
"""Initialize the instance."""
def assert_valid(
self, method_name: str, parameter_name: str, argument_value: Any
):
"""Raise an exception if method input is not valid.
:param method_name: Name of the method whose input is to be validated.
:param parameter_name: Name of the parameter whose input is to be
validated.
:param argument_value: Value of argument to parameter to be validated.
"""
# Try to import a custom validator class definition; if there isn't one,
# declare one that we can instantiate for the default argument to the
# constructor for {{contractName}} below.
@ -53,56 +34,52 @@ try:
)
except ImportError:
class {{contractName}}Validator({{contractName}}ValidatorBase): # type: ignore
class {{contractName}}Validator(Validator): # type: ignore
"""No-op input validator."""
{{tupleDefinitions ABIString}}
{{#each methods}}
{{> method_class contractName=../contractName}}
{{/each}}
# pylint: disable=too-many-public-methods
class {{contractName}}(BaseContractWrapper):
# pylint: disable=too-many-public-methods,too-many-instance-attributes
class {{contractName}}:
"""Wrapper class for {{contractName}} Solidity contract.{{docBytesIfNecessary ABIString}}"""
{{#each methods}}
{{toPythonIdentifier this.name}}: {{toPythonClassname this.name}}Method
{{/each}}
def __init__(
self,
provider: BaseProvider,
contract_address: str,
validator: {{contractName}}Validator = None,
private_key: str = None,
):
"""Get an instance of wrapper for smart contract.
:param provider: instance of :class:`web3.providers.base.BaseProvider`
:param contract_address: where the contract has been deployed
:param private_key: If specified, transactions will be signed locally,
via Web3.py's `eth.account.signTransaction()`:code:, before being
sent via `eth.sendRawTransaction()`:code:.
:param validator: for validation of method inputs.
"""
super().__init__(
provider=provider,
contract_address=contract_address,
private_key=private_key,
)
self.contract_address = contract_address
if not validator:
validator = {{contractName}}Validator(provider, contract_address, private_key)
validator = {{contractName}}Validator(provider, contract_address)
self.validator = validator
self._web3_eth = Web3( # type: ignore # pylint: disable=no-member
provider
).eth
def _get_contract_instance(self, token_address):
"""Get an instance of the smart contract at a specific address.
functions = self._web3_eth.contract(address=to_checksum_address(contract_address), abi={{contractName}}.abi()).functions
:returns: contract object
"""
return self._contract_instance(
address=token_address, abi={{contractName}}.abi()
)
{{#each methods}}
{{> call contractName=../contractName}}
{{/each}}
{{#each methods}}
self.{{toPythonIdentifier this.name}} = {{toPythonClassname this.name}}Method(provider, contract_address, functions.{{this.name}}, validator)
{{/each}}
{{#each events}}
{{> event}}
{{> event contractName=../contractName}}
{{/each}}
@staticmethod

View File

@ -1,54 +0,0 @@
def {{this.languageSpecificName}}(
self,
{{> typed_params inputs=inputs}}
tx_params: Optional[TxParams] = None,
{{^this.constant}}
view_only: bool = False,
{{/this.constant}}
) -> {{> return_type outputs=outputs~}}:
"""Execute underlying, same-named contract method.
{{sanitizeDevdocDetails this.name this.devdoc.details 8}}{{~#if this.devdoc.params~}}{{#each this.devdoc.params}}
{{makeParameterDocstringRole @key this 8}}{{/each}}{{/if}}
:param tx_params: transaction parameters
{{#if this.constant~}}
{{#if this.devdoc.return}}
{{makeReturnDocstringRole this.devdoc.return 8}}{{/if}}
{{else}}
:param view_only: whether to use transact() or call()
:returns: if param `view_only`:code: is `True`:code:, then returns the
value returned from the underlying function; else returns the
transaction hash.
{{/if}}
"""
{{#each this.inputs}}
self.validator.assert_valid(
method_name='{{../name}}',
parameter_name='{{name}}',
argument_value={{toPythonIdentifier name}},
)
{{#if (equal type 'address')}}
{{toPythonIdentifier this.name}} = self._validate_and_checksum_address({{toPythonIdentifier this.name}})
{{else if (equal type 'uint256')}}
# safeguard against fractional inputs
{{toPythonIdentifier this.name}} = int({{toPythonIdentifier this.name}})
{{else if (equal type 'bytes')}}
{{toPythonIdentifier this.name}} = bytes.fromhex({{toPythonIdentifier this.name}}.decode("utf-8"))
{{else if (equal type 'bytes[]')}}
{{toPythonIdentifier this.name}} = [
bytes.fromhex({{toPythonIdentifier this.name}}_element.decode("utf-8"))
for {{toPythonIdentifier this.name}}_element in {{toPythonIdentifier this.name}}
]
{{/if}}
{{/each}}
func = self._get_contract_instance(
self.contract_address
).functions.{{this.name}}(
{{> params}}
)
return self._invoke_function_call(
func=func,
tx_params=tx_params,
view_only={{#if this.constant}}True{{else}}view_only{{/if}}
)

View File

@ -6,8 +6,4 @@
{{makeEventParameterDocstringRole name 8}}
"""
tx_receipt = self._web3_eth.getTransactionReceipt(tx_hash)
return (
self._get_contract_instance(self.contract_address)
.events.{{name}}()
.processReceipt(tx_receipt)
)
return self._web3_eth.contract(address=to_checksum_address(self.contract_address), abi={{contractName}}.abi()).events.{{name}}().processReceipt(tx_receipt)

View File

@ -0,0 +1,76 @@
class {{toPythonClassname this.name}}Method(ContractMethod):
"""Various interfaces to the {{this.name}} method."""
def __init__(self, provider: BaseProvider, contract_address: str, contract_function: ContractFunction, validator: Validator=None):
"""Persist instance data."""
super().__init__(provider, contract_address, validator)
self.underlying_method = contract_function
{{#if inputs}}
def validate_and_normalize_inputs(self, {{> typed_params inputs=inputs}}):
"""Validate the inputs to the {{this.name}} method."""
{{#each this.inputs}}
self.validator.assert_valid(
method_name='{{../name}}',
parameter_name='{{name}}',
argument_value={{toPythonIdentifier name}},
)
{{#if (equal type 'address')}}
{{toPythonIdentifier this.name}} = self.validate_and_checksum_address({{toPythonIdentifier this.name}})
{{else if (equal type 'uint256')}}
# safeguard against fractional inputs
{{toPythonIdentifier this.name}} = int({{toPythonIdentifier this.name}})
{{else if (equal type 'bytes')}}
{{toPythonIdentifier this.name}} = bytes.fromhex({{toPythonIdentifier this.name}}.decode("utf-8"))
{{else if (equal type 'bytes[]')}}
{{toPythonIdentifier this.name}} = [
bytes.fromhex({{toPythonIdentifier this.name}}_element.decode("utf-8"))
for {{toPythonIdentifier this.name}}_element in {{toPythonIdentifier this.name}}
]
{{/if}}
{{/each}}
return ({{> params }})
{{/if}}
def call(self, {{#if inputs}}{{> typed_params inputs=inputs}}, {{/if}}tx_params: Optional[TxParams] = None) -> {{> return_type outputs=outputs type='call'~}}:
"""Execute underlying contract method via eth_call.
{{sanitizeDevdocDetails this.name this.devdoc.details 8}}{{~#if this.devdoc.params~}}{{#each this.devdoc.params}}
{{makeParameterDocstringRole @key this 8}}{{/each}}{{/if}}
:param tx_params: transaction parameters
{{#if this.constant~}}
{{#if this.devdoc.return}}
{{makeReturnDocstringRole this.devdoc.return 8}}{{/if}}
{{else}}
:returns: the return value of the underlying method.
{{/if}}
"""
{{#if inputs}}
({{> params }}) = self.validate_and_normalize_inputs({{> params}})
{{/if}}
tx_params = super().normalize_tx_params(tx_params)
return self.underlying_method({{> params}}).call(tx_params.as_dict())
def send_transaction(self, {{#if inputs}}{{> typed_params inputs=inputs}}, {{/if}}tx_params: Optional[TxParams] = None) -> Union[HexBytes, bytes]:
"""Execute underlying contract method via eth_sendTransaction.
{{sanitizeDevdocDetails this.name this.devdoc.details 8}}{{~#if this.devdoc.params~}}{{#each this.devdoc.params}}
{{makeParameterDocstringRole @key this 8}}{{/each}}{{/if}}
:param tx_params: transaction parameters
{{#if this.constant~}}
{{#if this.devdoc.return}}
{{makeReturnDocstringRole this.devdoc.return 8}}{{/if}}
{{/if}}
"""
{{#if inputs}}
({{> params }}) = self.validate_and_normalize_inputs({{> params}})
{{/if}}
tx_params = super().normalize_tx_params(tx_params)
return self.underlying_method({{> params}}).transact(tx_params.as_dict())
def estimate_gas(self, {{#if inputs}}{{> typed_params inputs=inputs}}, {{/if}}tx_params: Optional[TxParams] = None) -> int:
"""Estimate gas consumption of method call."""
{{#if inputs}}
({{> params }}) = self.validate_and_normalize_inputs({{> params}})
{{/if}}
tx_params = super().normalize_tx_params(tx_params)
return self.underlying_method({{> params}}).estimateGas(tx_params.as_dict())

View File

@ -1,3 +1,3 @@
{{#each inputs}}
{{toPythonIdentifier name}}{{#if @last}}{{else}},{{/if}}
{{/each}}
{{toPythonIdentifier name}}{{#if @last}}{{else}}, {{/if~}}
{{/each~}}

View File

@ -1,3 +1,3 @@
{{#each inputs}}
{{toPythonIdentifier name}}: {{#parameterType type components}}{{/parameterType}},
{{/each}}
{{toPythonIdentifier name}}: {{#parameterType type components}}{{/parameterType}}{{^if @last}}, {{/if~}}
{{/each~}}

View File

@ -1,12 +1,33 @@
[
{
"timestamp": 1564604963,
"version": "4.0.0",
"changes": [
{
"note": "whitespace changes to generated Python code",
"pr": 1996
},
{
"note": "move Python Validator base class from generated code to common package",
"pr": 1996
},
{
"note": "Changed fundamental thing-to-be-wrapped from the contract to the contract method. That is, now there is a base contract method wrapper class rather than a base contract wrapper class, and individual contract methods are represented by named classes inheriting from that base, and the different operations on a method are now represented by a nested-object dot notation, ie, WrappedContract.ContractMethod.call() and WrappedContract.ContractMethod.send_transaction().",
"pr": 1996
},
{
"note": "added gas estimation functionality to contract methods",
"pr": 1996
}
]
},
{
"version": "3.1.2",
"changes": [
{
"note": "Dependencies updated"
}
]
],
"timestamp": 1564604963
},
{
"version": "3.1.1",

View File

@ -23,6 +23,7 @@
"test_cli:clean": "rm -rf test-cli/output && rm -rf test-cli/test_typescript/lib",
"test_cli:build": "tsc --project test-cli/tsconfig.json",
"test_cli:run_mocha": "mocha --require source-map-support/register --require make-promises-safe test-cli/test_typescript/lib/**/*_test.js --timeout 100000 --bail --exit",
"test_cli:test_python": "black --check test-cli/output/python/**/__init__.py; test $? -le 1 # just make sure black can parse the output",
"rebuild_and_test": "run-s build test",
"test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html",
"test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha",

View File

@ -225,6 +225,10 @@ function registerPythonHelpers(): void {
}
return '';
});
Handlebars.registerHelper(
'toPythonClassname',
(sourceName: string) => new Handlebars.SafeString(changeCase.pascal(sourceName)),
);
}
if (args.language === 'TypeScript') {
registerTypeScriptHelpers();

View File

@ -11,37 +11,18 @@ from typing import ( # pylint: disable=unused-import
Union,
)
from eth_utils import to_checksum_address
from mypy_extensions import TypedDict # pylint: disable=unused-import
from hexbytes import HexBytes
from web3 import Web3
from web3.contract import ContractFunction
from web3.datastructures import AttributeDict
from web3.providers.base import BaseProvider
from zero_ex.contract_wrappers._base_contract_wrapper import BaseContractWrapper
from zero_ex.contract_wrappers.bases import ContractMethod, Validator
from zero_ex.contract_wrappers.tx_params import TxParams
class LibDummyValidatorBase:
"""Base class for validating inputs to LibDummy methods."""
def __init__(
self,
provider: BaseProvider,
contract_address: str,
private_key: str = None,
):
"""Initialize the instance."""
def assert_valid(
self, method_name: str, parameter_name: str, argument_value: Any
):
"""Raise an exception if method input is not valid.
:param method_name: Name of the method whose input is to be validated.
:param parameter_name: Name of the parameter whose input is to be
validated.
:param argument_value: Value of argument to parameter to be validated.
"""
# Try to import a custom validator class definition; if there isn't one,
# declare one that we can instantiate for the default argument to the
# constructor for LibDummy below.
@ -53,15 +34,15 @@ try:
)
except ImportError:
class LibDummyValidator(LibDummyValidatorBase): # type: ignore
class LibDummyValidator(Validator): # type: ignore
"""No-op input validator."""
# pylint: disable=too-many-public-methods
class LibDummy(BaseContractWrapper):
# pylint: disable=too-many-public-methods,too-many-instance-attributes
class LibDummy:
"""Wrapper class for LibDummy Solidity contract."""
def __init__(
@ -69,35 +50,24 @@ class LibDummy(BaseContractWrapper):
provider: BaseProvider,
contract_address: str,
validator: LibDummyValidator = None,
private_key: str = None,
):
"""Get an instance of wrapper for smart contract.
:param provider: instance of :class:`web3.providers.base.BaseProvider`
:param contract_address: where the contract has been deployed
:param private_key: If specified, transactions will be signed locally,
via Web3.py's `eth.account.signTransaction()`:code:, before being
sent via `eth.sendRawTransaction()`:code:.
:param validator: for validation of method inputs.
"""
super().__init__(
provider=provider,
contract_address=contract_address,
private_key=private_key,
)
self.contract_address = contract_address
if not validator:
validator = LibDummyValidator(provider, contract_address, private_key)
validator = LibDummyValidator(provider, contract_address)
self.validator = validator
self._web3_eth = Web3( # type: ignore # pylint: disable=no-member
provider
).eth
def _get_contract_instance(self, token_address):
"""Get an instance of the smart contract at a specific address.
functions = self._web3_eth.contract(address=to_checksum_address(contract_address), abi=LibDummy.abi()).functions
:returns: contract object
"""
return self._contract_instance(
address=token_address, abi=LibDummy.abi()
)
@staticmethod
def abi():

View File

@ -11,37 +11,18 @@ from typing import ( # pylint: disable=unused-import
Union,
)
from eth_utils import to_checksum_address
from mypy_extensions import TypedDict # pylint: disable=unused-import
from hexbytes import HexBytes
from web3 import Web3
from web3.contract import ContractFunction
from web3.datastructures import AttributeDict
from web3.providers.base import BaseProvider
from zero_ex.contract_wrappers._base_contract_wrapper import BaseContractWrapper
from zero_ex.contract_wrappers.bases import ContractMethod, Validator
from zero_ex.contract_wrappers.tx_params import TxParams
class TestLibDummyValidatorBase:
"""Base class for validating inputs to TestLibDummy methods."""
def __init__(
self,
provider: BaseProvider,
contract_address: str,
private_key: str = None,
):
"""Initialize the instance."""
def assert_valid(
self, method_name: str, parameter_name: str, argument_value: Any
):
"""Raise an exception if method input is not valid.
:param method_name: Name of the method whose input is to be validated.
:param parameter_name: Name of the parameter whose input is to be
validated.
:param argument_value: Value of argument to parameter to be validated.
"""
# Try to import a custom validator class definition; if there isn't one,
# declare one that we can instantiate for the default argument to the
# constructor for TestLibDummy below.
@ -53,62 +34,23 @@ try:
)
except ImportError:
class TestLibDummyValidator(TestLibDummyValidatorBase): # type: ignore
class TestLibDummyValidator(Validator): # type: ignore
"""No-op input validator."""
# pylint: disable=too-many-public-methods
class TestLibDummy(BaseContractWrapper):
"""Wrapper class for TestLibDummy Solidity contract."""
class PublicAddConstantMethod(ContractMethod):
"""Various interfaces to the publicAddConstant method."""
def __init__(
self,
provider: BaseProvider,
contract_address: str,
validator: TestLibDummyValidator = None,
private_key: str = None,
):
"""Get an instance of wrapper for smart contract.
def __init__(self, provider: BaseProvider, contract_address: str, contract_function: ContractFunction, validator: Validator=None):
"""Persist instance data."""
super().__init__(provider, contract_address, validator)
self.underlying_method = contract_function
:param provider: instance of :class:`web3.providers.base.BaseProvider`
:param contract_address: where the contract has been deployed
:param private_key: If specified, transactions will be signed locally,
via Web3.py's `eth.account.signTransaction()`:code:, before being
sent via `eth.sendRawTransaction()`:code:.
"""
super().__init__(
provider=provider,
contract_address=contract_address,
private_key=private_key,
)
if not validator:
validator = TestLibDummyValidator(provider, contract_address, private_key)
self.validator = validator
def _get_contract_instance(self, token_address):
"""Get an instance of the smart contract at a specific address.
:returns: contract object
"""
return self._contract_instance(
address=token_address, abi=TestLibDummy.abi()
)
def public_add_constant(
self,
x: int,
tx_params: Optional[TxParams] = None,
) -> int:
"""Execute underlying, same-named contract method.
:param tx_params: transaction parameters
"""
def validate_and_normalize_inputs(self, x: int):
"""Validate the inputs to the publicAddConstant method."""
self.validator.assert_valid(
method_name='publicAddConstant',
parameter_name='x',
@ -116,27 +58,44 @@ class TestLibDummy(BaseContractWrapper):
)
# safeguard against fractional inputs
x = int(x)
func = self._get_contract_instance(
self.contract_address
).functions.publicAddConstant(
x
)
return self._invoke_function_call(
func=func,
tx_params=tx_params,
view_only=True
)
return (x)
def public_add_one(
self,
x: int,
tx_params: Optional[TxParams] = None,
) -> int:
"""Execute underlying, same-named contract method.
def call(self, x: int, tx_params: Optional[TxParams] = None) -> int:
"""Execute underlying contract method via eth_call.
:param tx_params: transaction parameters
"""
(x) = self.validate_and_normalize_inputs(x)
tx_params = super().normalize_tx_params(tx_params)
return self.underlying_method(x).call(tx_params.as_dict())
def send_transaction(self, x: int, tx_params: Optional[TxParams] = None) -> Union[HexBytes, bytes]:
"""Execute underlying contract method via eth_sendTransaction.
:param tx_params: transaction parameters
"""
(x) = self.validate_and_normalize_inputs(x)
tx_params = super().normalize_tx_params(tx_params)
return self.underlying_method(x).transact(tx_params.as_dict())
def estimate_gas(self, x: int, tx_params: Optional[TxParams] = None) -> int:
"""Estimate gas consumption of method call."""
(x) = self.validate_and_normalize_inputs(x)
tx_params = super().normalize_tx_params(tx_params)
return self.underlying_method(x).estimateGas(tx_params.as_dict())
class PublicAddOneMethod(ContractMethod):
"""Various interfaces to the publicAddOne method."""
def __init__(self, provider: BaseProvider, contract_address: str, contract_function: ContractFunction, validator: Validator=None):
"""Persist instance data."""
super().__init__(provider, contract_address, validator)
self.underlying_method = contract_function
def validate_and_normalize_inputs(self, x: int):
"""Validate the inputs to the publicAddOne method."""
self.validator.assert_valid(
method_name='publicAddOne',
parameter_name='x',
@ -144,16 +103,67 @@ class TestLibDummy(BaseContractWrapper):
)
# safeguard against fractional inputs
x = int(x)
func = self._get_contract_instance(
self.contract_address
).functions.publicAddOne(
x
)
return self._invoke_function_call(
func=func,
tx_params=tx_params,
view_only=True
)
return (x)
def call(self, x: int, tx_params: Optional[TxParams] = None) -> int:
"""Execute underlying contract method via eth_call.
:param tx_params: transaction parameters
"""
(x) = self.validate_and_normalize_inputs(x)
tx_params = super().normalize_tx_params(tx_params)
return self.underlying_method(x).call(tx_params.as_dict())
def send_transaction(self, x: int, tx_params: Optional[TxParams] = None) -> Union[HexBytes, bytes]:
"""Execute underlying contract method via eth_sendTransaction.
:param tx_params: transaction parameters
"""
(x) = self.validate_and_normalize_inputs(x)
tx_params = super().normalize_tx_params(tx_params)
return self.underlying_method(x).transact(tx_params.as_dict())
def estimate_gas(self, x: int, tx_params: Optional[TxParams] = None) -> int:
"""Estimate gas consumption of method call."""
(x) = self.validate_and_normalize_inputs(x)
tx_params = super().normalize_tx_params(tx_params)
return self.underlying_method(x).estimateGas(tx_params.as_dict())
# pylint: disable=too-many-public-methods,too-many-instance-attributes
class TestLibDummy:
"""Wrapper class for TestLibDummy Solidity contract."""
public_add_constant: PublicAddConstantMethod
public_add_one: PublicAddOneMethod
def __init__(
self,
provider: BaseProvider,
contract_address: str,
validator: TestLibDummyValidator = None,
):
"""Get an instance of wrapper for smart contract.
:param provider: instance of :class:`web3.providers.base.BaseProvider`
:param contract_address: where the contract has been deployed
:param validator: for validation of method inputs.
"""
self.contract_address = contract_address
if not validator:
validator = TestLibDummyValidator(provider, contract_address)
self._web3_eth = Web3( # type: ignore # pylint: disable=no-member
provider
).eth
functions = self._web3_eth.contract(address=to_checksum_address(contract_address), abi=TestLibDummy.abi()).functions
self.public_add_constant = PublicAddConstantMethod(provider, contract_address, functions.publicAddConstant, validator)
self.public_add_one = PublicAddOneMethod(provider, contract_address, functions.publicAddOne, validator)
@staticmethod
def abi():

View File

@ -109,7 +109,8 @@ class TestCommandExtension(TestCommand):
"""Invoke pytest."""
import pytest
exit(pytest.main(["--doctest-modules"]))
exit(pytest.main(["--doctest-modules", "-rapP"]))
# show short test summary at end ^
with open("README.md", "r") as file_handle:

View File

@ -138,7 +138,8 @@ class TestCommandExtension(TestCommand):
"""Invoke pytest."""
import pytest
exit(pytest.main(["--doctest-modules"]))
exit(pytest.main(["--doctest-modules", "-rapP"]))
# show short test summary at end ^
with open("README.md", "r") as file_handle:

View File

@ -0,0 +1,5 @@
[pycodestyle]
ignore = E501, W503
# E501 = line too long
# W503 = line break occurred before a binary operator
# we let black handle these things

View File

@ -74,7 +74,8 @@ class TestCommandExtension(TestCommand):
"""Invoke pytest."""
import pytest
exit(pytest.main(["--doctest-modules"]))
exit(pytest.main(["--doctest-modules", "-rapP"]))
# show short test summary at end ^
class LintCommand(distutils.command.build_py.build_py):

View File

@ -102,13 +102,13 @@ balance:
>>> erc20_proxy_addr = NETWORK_TO_ADDRESSES[NetworkId.GANACHE].erc20_proxy
>>> tx = zrx_token.approve(
>>> tx = zrx_token.approve.send_transaction(
... erc20_proxy_addr,
... to_wei(100, 'ether'),
... tx_params=TxParams(from_=maker_address),
... )
>>> tx = weth_token.approve(
>>> tx = weth_token.approve.send_transaction(
... erc20_proxy_addr,
... to_wei(100, 'ether'),
... tx_params=TxParams(from_=taker_address),
@ -166,7 +166,7 @@ too.
... provider=ganache,
... contract_address=NETWORK_TO_ADDRESSES[NetworkId.GANACHE].exchange,
... )
>>> tx_hash = exchange.fill_order(
>>> tx_hash = exchange.fill_order.send_transaction(
... order=order,
... taker_asset_fill_amount=order["takerAssetAmount"],
... signature=maker_signature,
@ -217,7 +217,7 @@ A Maker can cancel an order that has yet to be filled.
... )
... )
>>> tx_hash = exchange.cancel_order(
>>> tx_hash = exchange.cancel_order.send_transaction(
... order=order, tx_params=TxParams(from_=maker_address)
... )
@ -287,12 +287,40 @@ is an example where the taker fills two orders in one transaction:
Fill order_1 and order_2 together:
>>> exchange.batch_fill_orders(
>>> exchange.batch_fill_orders.send_transaction(
... orders=[order_1, order_2],
... taker_asset_fill_amounts=[1, 2],
... signatures=[signature_1, signature_2],
... tx_params=TxParams(from_=taker_address))
HexBytes('0x...')
Estimating gas consumption
--------------------------
Before executing a transaction, you may want to get an estimate of how much gas
will be consumed.
>>> exchange.cancel_order.estimate_gas(
... order=Order(
... makerAddress=maker_address,
... takerAddress='0x0000000000000000000000000000000000000000',
... exchangeAddress=exchange_address,
... senderAddress='0x0000000000000000000000000000000000000000',
... feeRecipientAddress='0x0000000000000000000000000000000000000000',
... makerAssetData=asset_data_utils.encode_erc20(weth_address),
... takerAssetData=asset_data_utils.encode_erc20(weth_address),
... salt=random.randint(1, 100000000000000000),
... makerFee=0,
... takerFee=0,
... makerAssetAmount=1000000000000000000,
... takerAssetAmount=500000000000000000000,
... expirationTimeSeconds=round(
... (datetime.utcnow() + timedelta(days=1)).timestamp()
... )
... ),
... tx_params=TxParams(from_=maker_address),
... )
73825
"""
from .tx_params import TxParams

View File

@ -1,136 +0,0 @@
"""Base wrapper class for accessing ethereum smart contracts."""
from typing import Optional, Union
from eth_utils import to_checksum_address
from web3 import Web3
from web3.providers.base import BaseProvider
from .tx_params import TxParams
class BaseContractWrapper:
"""Base class for wrapping ethereum smart contracts.
It provides functionality for instantiating a contract instance,
calling view functions, and calling functions which require
transactions.
"""
def __init__(
self,
provider: BaseProvider,
contract_address: str,
private_key: str = None,
):
"""Create an instance of BaseContractWrapper.
:param provider: instance of :class:`web3.providers.base.BaseProvider`
:param private_key: If specified, transactions will be signed locally,
via Web3.py's `eth.account.signTransaction()`:code:, before being
sent via `eth.sendRawTransaction()`:code:.
"""
self._provider = provider
self._private_key = private_key
self._web3 = Web3(provider)
self._web3_eth = self._web3.eth # pylint: disable=no-member
self.contract_address = self._validate_and_checksum_address(
contract_address
)
self._can_send_tx = False
if self._web3_eth.defaultAccount or self._web3_eth.accounts:
self._can_send_tx = True
else:
middleware_stack = getattr(self._web3, "middleware_stack")
if middleware_stack.get("sign_and_send_raw_middleware"):
self._can_send_tx = True
elif private_key:
self._private_key = private_key
self._web3_eth.defaultAccount = to_checksum_address(
self._web3_eth.account.privateKeyToAccount(
private_key
).address
)
self._can_send_tx = True
def _contract_instance(self, address: str, abi: dict):
"""Get a contract instance.
:param address: string address of contract
:param abi: dict contract ABI
:returns: instance of contract
"""
return self._web3_eth.contract(
address=to_checksum_address(address), abi=abi
)
def _validate_and_checksum_address(self, address: str):
if not self._web3.isAddress(address):
raise TypeError("Invalid address provided: {}".format(address))
return to_checksum_address(address)
def _invoke_function_call(self, func, tx_params, view_only):
if view_only:
return func.call()
if not self._can_send_tx:
raise Exception(
"Cannot send transaction because no local private_key"
" or account found."
)
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._validate_and_checksum_address(tx_params.from_)
if self._private_key:
res = self._sign_and_send_raw_direct(func, tx_params)
else:
res = func.transact(tx_params.as_dict())
return res
def _sign_and_send_raw_direct(self, func, tx_params):
transaction = func.buildTransaction(tx_params.as_dict())
signed_tx = self._web3_eth.account.signTransaction(
transaction, private_key=self._private_key
)
return self._web3_eth.sendRawTransaction(signed_tx.rawTransaction)
# pylint: disable=too-many-arguments
def execute_method(
self,
abi: dict,
method: str,
args: Optional[Union[list, tuple]] = None,
tx_params: Optional[TxParams] = None,
view_only: bool = False,
) -> str:
"""Execute the method on a contract instance.
:param abi: dict of contract ABI
:param method: string name of method to call
:param args: default None, list or tuple of arguments for the method
:param tx_params: default None, :class:`TxParams` transaction params
:param view_only: default False, boolean of whether the transaction
should only be validated.
:returns: str of transaction hash
"""
contract_instance = self._contract_instance(
address=self.contract_address, abi=abi
)
if args is None:
args = []
if hasattr(contract_instance.functions, method):
func = getattr(contract_instance.functions, method)(*args)
return self._invoke_function_call(
func=func, tx_params=tx_params, view_only=view_only
)
raise Exception(
"No method {} found on contract {}.".format(
self.contract_address, method
)
)

View File

@ -0,0 +1,66 @@
"""Base wrapper class for accessing ethereum smart contracts."""
from typing import Any
from eth_utils import is_address, to_checksum_address
from web3 import Web3
from web3.providers.base import BaseProvider
from .tx_params import TxParams
class Validator:
"""Base class for validating inputs to methods."""
def __init__(self, provider: BaseProvider, contract_address: str):
"""Initialize the instance."""
def assert_valid(
self, method_name: str, parameter_name: str, argument_value: Any
):
"""Raise an exception if method input is not valid.
:param method_name: Name of the method whose input is to be validated.
:param parameter_name: Name of the parameter whose input is to be
validated.
:param argument_value: Value of argument to parameter to be validated.
"""
class ContractMethod:
"""Base class for wrapping an Ethereum smart contract method."""
def __init__(
self,
provider: BaseProvider,
contract_address: str,
validator: Validator = None,
):
"""Instantiate the object.
:param provider: Instance of :class:`web3.providers.base.BaseProvider`
: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
if validator is None:
validator = Validator(provider, contract_address)
self.validator = validator
@staticmethod
def validate_and_checksum_address(address: str):
"""Validate the given address, and return it's checksum address."""
if not is_address(address):
raise TypeError("Invalid address provided: {}".format(address))
return to_checksum_address(address)
def normalize_tx_params(self, tx_params) -> TxParams:
"""Normalize and return the given transaction parameters."""
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.validate_and_checksum_address(tx_params.from_)
return tx_params

View File

@ -6,21 +6,16 @@ from web3.providers.base import BaseProvider
from zero_ex import json_schemas
from . import ExchangeValidatorBase
from ..bases import Validator
from .types import order_to_jsdict
class ExchangeValidator(ExchangeValidatorBase):
class ExchangeValidator(Validator):
"""Validate inputs to Exchange methods."""
def __init__(
self,
provider: BaseProvider,
contract_address: str,
private_key: str = None,
):
def __init__(self, provider: BaseProvider, contract_address: str):
"""Initialize the class."""
super().__init__(provider, contract_address, private_key)
super().__init__(provider, contract_address)
self.contract_address = contract_address
def assert_valid(

View File

@ -1,3 +1,7 @@
from typing import Union
def to_checksum_address(address: str) -> str: ...
def remove_0x_prefix(hex_string: str) -> str: ...
def remove_0x_prefix(hex_string: str) -> str: ...
def is_address(address: Union[str, bytes]) -> bool: ...

View File

@ -0,0 +1,5 @@
class ContractFunction:
def __call__(self, *args, **kwargs):
...
...

View File

@ -1,3 +1,10 @@
from typing import Any
class Contract:
def call(self): ...
functions: Any
events: Any
...

View File

@ -0,0 +1,15 @@
"""Tests for :class:`ContractMethod`."""
import pytest
from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES, NetworkId
from zero_ex.contract_wrappers.bases import ContractMethod
@pytest.fixture(scope="module")
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,
)

View File

@ -1,48 +0,0 @@
"""Tests for :class:`BaseContractWrapper`."""
import pytest
from eth_utils import to_checksum_address
from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES, NetworkId
from zero_ex.contract_artifacts import abi_by_name
from zero_ex.contract_wrappers._base_contract_wrapper import (
BaseContractWrapper,
)
@pytest.fixture(scope="module")
def contract_wrapper(ganache_provider):
"""Get a BaseContractWrapper instance for testing."""
return BaseContractWrapper(
provider=ganache_provider,
contract_address=NETWORK_TO_ADDRESSES[NetworkId.GANACHE].ether_token,
)
def test_contract_wrapper__execute_method(
accounts,
contract_wrapper, # pylint: disable=redefined-outer-name
erc20_proxy_address,
):
"""Test :function:`BaseContractWrapper.execute` method."""
acc1_allowance = contract_wrapper.execute_method(
abi=abi_by_name("WETH9"),
method="allowance",
view_only=True,
args=(
to_checksum_address(accounts[3]),
to_checksum_address(erc20_proxy_address),
),
)
assert acc1_allowance == 0
with pytest.raises(Exception):
contract_wrapper.execute_method(
abi=abi_by_name("WETH9"),
method="send",
view_only=True,
args=[
to_checksum_address(accounts[3]),
to_checksum_address(erc20_proxy_address),
],
)

View File

@ -25,8 +25,8 @@ def test_erc20_wrapper__balance_of(
weth_instance, # pylint: disable=redefined-outer-name
):
"""Test getting baance of an account for an ERC20 token."""
acc1_original_weth_balance = erc20_wrapper.balance_of(accounts[0])
acc2_original_weth_balance = erc20_wrapper.balance_of(accounts[1])
acc1_original_weth_balance = erc20_wrapper.balance_of.call(accounts[0])
acc2_original_weth_balance = erc20_wrapper.balance_of.call(accounts[1])
expected_difference = 1 * 10 ** 18
@ -36,8 +36,8 @@ def test_erc20_wrapper__balance_of(
weth_instance.functions.deposit().transact(
{"from": accounts[1], "value": expected_difference}
)
acc1_weth_balance = erc20_wrapper.balance_of(accounts[0])
acc2_weth_balance = erc20_wrapper.balance_of(accounts[1])
acc1_weth_balance = erc20_wrapper.balance_of.call(accounts[0])
acc2_weth_balance = erc20_wrapper.balance_of.call(accounts[1])
assert (
acc1_weth_balance - acc1_original_weth_balance == expected_difference
@ -53,21 +53,21 @@ def test_erc20_wrapper__approve(
erc20_wrapper, # pylint: disable=redefined-outer-name
):
"""Test approving one account to spend balance from another account."""
erc20_wrapper.approve(
erc20_wrapper.approve.send_transaction(
erc20_proxy_address,
MAX_ALLOWANCE,
tx_params=TxParams(from_=accounts[0]),
)
erc20_wrapper.approve(
erc20_wrapper.approve.send_transaction(
erc20_proxy_address,
MAX_ALLOWANCE,
tx_params=TxParams(from_=accounts[1]),
)
acc_1_weth_allowance = erc20_wrapper.allowance(
acc_1_weth_allowance = erc20_wrapper.allowance.call(
accounts[0], erc20_proxy_address
)
acc_2_weth_allowance = erc20_wrapper.allowance(
acc_2_weth_allowance = erc20_wrapper.allowance.call(
accounts[1], erc20_proxy_address
)

View File

@ -77,7 +77,7 @@ def test_exchange_wrapper__fill_order(
)
order_signature = sign_hash_to_bytes(ganache_provider, maker, order_hash)
tx_hash = exchange_wrapper.fill_order(
tx_hash = exchange_wrapper.fill_order.send_transaction(
order=order,
taker_asset_fill_amount=order["takerAssetAmount"],
signature=order_signature,
@ -114,7 +114,7 @@ def test_exchange_wrapper__batch_fill_orders(
for order_hash in order_hashes
]
taker_amounts = [order["takerAssetAmount"] for order in orders]
tx_hash = exchange_wrapper.batch_fill_orders(
tx_hash = exchange_wrapper.batch_fill_orders.send_transaction(
orders=orders,
taker_asset_fill_amounts=taker_amounts,
signatures=order_signatures,

View File

@ -40,7 +40,8 @@ class TestCommandExtension(TestCommand):
"""Invoke pytest."""
import pytest
exit(pytest.main(["--doctest-modules"]))
exit(pytest.main(["--doctest-modules", "-rapP"]))
# show short test summary at end ^
class LintCommand(distutils.command.build_py.build_py):

View File

@ -20,7 +20,8 @@ class TestCommandExtension(TestCommand):
"""Invoke pytest."""
import pytest
exit(pytest.main(["--doctest-modules"]))
exit(pytest.main(["--doctest-modules", "-rapP"]))
# show short test summary at end ^
class LintCommand(distutils.command.build_py.build_py):

View File

@ -21,7 +21,8 @@ class TestCommandExtension(TestCommand):
"""Invoke pytest."""
import pytest
exit(pytest.main(["--doctest-modules"]))
exit(pytest.main(["--doctest-modules", "-rapP"]))
# show short test summary at end ^
class LintCommand(distutils.command.build_py.build_py):

View File

@ -20,9 +20,9 @@ $ ./parallel pip uninstall $(basename $(pwd))
>>>"""
from concurrent.futures import ProcessPoolExecutor
from concurrent.futures import ProcessPoolExecutor, wait
from os import chdir
from subprocess import check_call
from subprocess import CalledProcessError, check_output
from sys import argv
PACKAGES = [
@ -38,7 +38,18 @@ PACKAGES = [
def run_cmd_on_package(package: str):
"""cd to the package dir, ./setup.py lint, cd .."""
chdir(package)
check_call(f"{' '.join(argv[1:])}".split())
chdir("..")
try:
check_output(f"{' '.join(argv[1:])}".split())
except CalledProcessError as error:
print(f"standard output from command:\n{error.output.decode('utf-8')}")
raise RuntimeError(f"Above exception raised in {package}, ") from error
finally:
chdir("..")
ProcessPoolExecutor().map(run_cmd_on_package, PACKAGES)
with ProcessPoolExecutor() as executor:
for future in executor.map(run_cmd_on_package, PACKAGES):
# iterate over map()'s return value, to resolve the futures.
# but we don't actually care what the return values are, so just `pass`.
# if any exceptions were raised by the underlying task, they'll be
# raised as the iteration encounters them.
pass

View File

@ -33,7 +33,8 @@ class TestCommandExtension(TestCommand):
"""Invoke pytest."""
import pytest
exit(pytest.main(["--doctest-modules"]))
exit(pytest.main(["--doctest-modules", "-rapP"]))
# show short test summary at end ^
class TestPublishCommand(distutils.command.build_py.build_py):

View File

@ -319,7 +319,7 @@ book. Now let's have the taker fill it:
... provider=eth_node,
... contract_address=NETWORK_TO_ADDRESSES[NetworkId.GANACHE].exchange
... )
>>> exchange.fill_order(
>>> exchange.fill_order.send_transaction(
... order=order,
... taker_asset_fill_amount=order['makerAssetAmount']/2, # note the half fill
... signature=order['signature'].replace('0x', '').encode('utf-8'),
@ -333,7 +333,7 @@ Cancelling
Note that the above fill was partial: it only filled half of the order. Now
we'll have our maker cancel the remaining order:
>>> exchange.cancel_order(
>>> exchange.cancel_order.send_transaction(
... order=order,
... tx_params=TxParams(from_=maker_address)
... )