Exemple #1
0
class DSProxy(Contract):
    """A client for the `DSProxy` contract.

    You can find the source code of the `DSProxy` contract here:
    <https://github.com/dapphub/ds-proxy>.

    Attributes:
        web3: An instance of `Web` from `web3.py`.
        address: Ethereum address of the `DSProxy` contract.
    """

    abi = Contract._load_abi(__name__, 'abi/DSProxy.abi')
    bin = Contract._load_bin(__name__, 'abi/DSProxy.bin')

    def __init__(self, web3: Web3, address: Address):
        self.web3 = web3
        self.address = address
        self._contract = self._get_contract(web3, self.abi, address)

    def execute(self, contract: bytes, calldata: bytes) -> Transact:
        """Create a new contract and call a function of it.

        Creates a new contract using the bytecode (`contract`). Then does _delegatecall_ to the function
        and arguments specified in the `calldata`.

        Args:
            contract: Contract bytecode.
            calldata: Calldata to pass to _delegatecall_.
        """
        return Transact(self, self.web3, self.abi, self.address,
                        self._contract, 'execute', [contract, calldata])

    def __repr__(self):
        return f"DSProxy('{self.address}')"
Exemple #2
0
class DSRoles(Contract):
    """A client for the `DSRoles` contract.

    You can find the source code of the `DSRoles` contract here:
    <https://github.com/dapphub/ds-roles>.

    Attributes:
        web3: An instance of `Web` from `web3.py`.
        address: Ethereum address of the `DSRoles` contract.
    """

    abi = Contract._load_abi(__name__, 'abi/DSRoles.abi')
    bin = Contract._load_bin(__name__, 'abi/DSRoles.bin')

    def __init__(self, web3, address):
        self.web3 = web3
        self.address = address
        self._contract = web3.eth.contract(abi=self.abi)(address=address.address)

    @staticmethod
    def deploy(web3: Web3):
        return DSRoles(web3=web3, address=Contract._deploy(web3, DSRoles.abi, DSRoles.bin, []))

    def __repr__(self):
        return f"DSRoles('{self.address}')"
Exemple #3
0
class ExpiringMarket(SimpleMarket):
    """A client for a `ExpiringMarket` contract.

    You can find the source code of the `OasisDEX` contracts here:
    <https://github.com/makerdao/maker-otc>.

    Attributes:
        web3: An instance of `Web` from `web3.py`.
        address: Ethereum address of the `ExpiringMarket` contract.
    """

    abi = Contract._load_abi(__name__, 'abi/ExpiringMarket.abi')
    bin = Contract._load_bin(__name__, 'abi/ExpiringMarket.bin')

    @staticmethod
    def deploy(web3: Web3, close_time: int):
        """Deploy a new instance of the `ExpiringMarket` contract.

        Args:
            web3: An instance of `Web` from `web3.py`.
            close_time: Unix timestamp of when the market will close.

        Returns:
            A `ExpiringMarket` class instance.
        """
        return ExpiringMarket(web3=web3,
                              address=Contract._deploy(web3,
                                                       ExpiringMarket.abi,
                                                       ExpiringMarket.bin,
                                                       [close_time]))

    def __repr__(self):
        return f"ExpiringMarket('{self.address}')"
Exemple #4
0
class DSVault(Contract):
    """A client for the `DSVault` contract.

    You can find the source code of the `DSVault` contract here:
    <https://github.com/dapphub/ds-vault>.

    Attributes:
        web3: An instance of `Web` from `web3.py`.
        address: Ethereum address of the `DSVault` contract.
    """

    abi = Contract._load_abi(__name__, 'abi/DSVault.abi')
    bin = Contract._load_bin(__name__, 'abi/DSVault.bin')

    def __init__(self, web3, address):
        self.web3 = web3
        self.address = address
        self._contract = web3.eth.contract(abi=self.abi)(
            address=address.address)

    @staticmethod
    def deploy(web3: Web3):
        """Deploy a new instance of the `DSVault` contract.

        Args:
            web3: An instance of `Web` from `web3.py`.

        Returns:
            A `DSVault` class instance.
        """
        return DSVault(web3=web3,
                       address=Contract._deploy(web3, DSVault.abi, DSVault.bin,
                                                []))

    def authority(self) -> Address:
        """Return the current `authority` of a `DSAuth`-ed contract.

        Returns:
            The address of the current `authority`.
        """
        return Address(self._contract.call().authority())

    def set_authority(self, address: Address) -> Transact:
        """Set the `authority` of a `DSAuth`-ed contract.

        Args:
            address: The address of the new `authority`.

        Returns:
            A `Transact` instance, which can be used to trigger the transaction.
        """
        assert (isinstance(address, Address))
        return Transact(self, self.web3, self.abi, self.address,
                        self._contract, 'setAuthority', [address.address])

    def __repr__(self):
        return f"DSVault('{self.address}')"
Exemple #5
0
class DSGuard(Contract):
    """A client for the `DSGuard` contract.

    You can find the source code of the `DSGuard` contract here:
    <https://github.com/dapphub/ds-guard>.

    Attributes:
        web3: An instance of `Web` from `web3.py`.
        address: Ethereum address of the `DSGuard` contract.
    """

    abi = Contract._load_abi(__name__, 'abi/DSGuard.abi')
    bin = Contract._load_bin(__name__, 'abi/DSGuard.bin')

    ANY = int_to_bytes32(2 ** 256 - 1)

    def __init__(self, web3, address):
        self.web3 = web3
        self.address = address
        self._contract = web3.eth.contract(abi=self.abi)(address=address.address)

    @staticmethod
    def deploy(web3: Web3):
        return DSGuard(web3=web3, address=Contract._deploy(web3, DSGuard.abi, DSGuard.bin, []))

    def permit(self, src, dst, sig: bytes) -> Transact:
        assert(isinstance(src, Address) or isinstance(src, bytes))
        assert(isinstance(dst, Address) or isinstance(dst, bytes))
        assert(isinstance(sig, bytes) and len(sig) == 32)

        if isinstance(src, Address):
            src = src.address
        if isinstance(dst, Address):
            dst = dst.address

        return Transact(self, self.web3, self.abi, self.address, self._contract, 'permit', [src, dst, sig])

    def forbid(self, src: Address, dst: Address, sig: bytes) -> Transact:
        assert(isinstance(src, Address) or isinstance(src, bytes))
        assert(isinstance(dst, Address) or isinstance(dst, bytes))
        assert(isinstance(sig, bytes) and len(sig) == 32)

        if isinstance(src, Address):
            src = src.address
        if isinstance(dst, Address):
            dst = dst.address

        return Transact(self, self.web3, self.abi, self.address, self._contract, 'forbid', [src, dst, sig])

    def __repr__(self):
        return f"DSGuard('{self.address}')"
Exemple #6
0
class Top(Contract):
    """A client for the `Top` contract, one of the `SAI Stablecoin System` contracts.

    Attributes:
        web3: An instance of `Web` from `web3.py`.
        address: Ethereum address of the `Top` contract.
    """

    abi = Contract._load_abi(__name__, 'abi/Top.abi')
    bin = Contract._load_bin(__name__, 'abi/Top.bin')

    def __init__(self, web3: Web3, address: Address):
        self.web3 = web3
        self.address = address
        self._contract = self._get_contract(web3, self.abi, address)

    @staticmethod
    def deploy(web3: Web3, tub: Address, tap: Address):
        assert (isinstance(tub, Address))
        assert (isinstance(tap, Address))
        return Top(web3=web3,
                   address=Contract._deploy(web3, Top.abi, Top.bin,
                                            [tub.address, tap.address]))

    def set_authority(self, address: Address) -> Transact:
        assert (isinstance(address, Address))
        return Transact(self, self.web3, self.abi, self.address,
                        self._contract, 'setAuthority', [address.address])

    def fix(self) -> Ray:
        """Get the GEM per SAI settlement price.

        Returns:
            The GEM per SAI settlement (kill) price.
        """
        return Ray(self._contract.call().fix())

    # TODO cage
    # TODO cash
    # TODO vent

    def __eq__(self, other):
        assert (isinstance(other, Top))
        return self.address == other.address

    def __repr__(self):
        return f"Top('{self.address}')"
Exemple #7
0
class TxManager(Contract):
    """A client for the `TxManager` contract.

    `TxManager` allows to invoke multiple contract methods in one Ethereum transaction.
    Each invocation is represented as an instance of the `Invocation` class, containing a
    contract address and a calldata.

    In addition to that, these invocations can use ERC20 token balances. In order to do that,
    the entire allowance of each token involved is transferred from the caller to the `TxManager`
    contract at the beginning of the transaction and all the remaining balances are returned
    to the caller at the end of it. In order to use this feature, ERC20 token allowances
    have to be granted to the `TxManager`.

    You can find the source code of the `TxManager` contract here:
    <https://github.com/reverendus/tx-manager>.

    Attributes:
        web3: An instance of `Web` from `web3.py`.
        address: Ethereum address of the `TxManager` contract.
    """

    abi = Contract._load_abi(__name__, 'abi/TxManager.abi')
    bin = Contract._load_bin(__name__, 'abi/TxManager.bin')

    def __init__(self, web3: Web3, address: Address):
        self.web3 = web3
        self.address = address
        self._contract = self._get_contract(web3, self.abi, address)

    @staticmethod
    def deploy(web3: Web3):
        return TxManager(web3=web3,
                         address=Contract._deploy(web3, TxManager.abi,
                                                  TxManager.bin, []))

    def approve(self, tokens: List[ERC20Token], approval_function):
        for token in tokens:
            approval_function(token, self.address, 'TxManager')

    def owner(self) -> Address:
        return Address(self._contract.call().owner())

    def execute(self, tokens: List[Address],
                invocations: List[Invocation]) -> Transact:
        """Executes multiple contract methods in one Ethereum transaction.

        Args:
            tokens: List of addresses of ERC20 token the invocations should be able to access.
            invocations: A list of invocations (contract methods) to be executed.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        def token_addresses() -> list:
            return list(map(lambda address: address.address, tokens))

        def script() -> bytes:
            return reduce(
                operator.add,
                map(lambda invocation: script_entry(invocation), invocations),
                bytes())

        def script_entry(invocation: Invocation) -> bytes:
            address = invocation.address.as_bytes()
            calldata = invocation.calldata.as_bytes()
            calldata_length = len(calldata).to_bytes(32, byteorder='big')
            return address + calldata_length + calldata

        assert (isinstance(tokens, list))
        assert (isinstance(invocations, list))

        return Transact(self, self.web3, self.abi, self.address,
                        self._contract, 'execute',
                        [token_addresses(), script()])
Exemple #8
0
class Tap(Contract):
    """A client for the `Tap` contract, on of the contracts driving the `SAI Stablecoin System`.

    Attributes:
        web3: An instance of `Web` from `web3.py`.
        address: Ethereum address of the `Tap` contract.
    """

    abi = Contract._load_abi(__name__, 'abi/Tap.abi')
    bin = Contract._load_bin(__name__, 'abi/Tap.bin')

    def __init__(self, web3: Web3, address: Address):
        self.web3 = web3
        self.address = address
        self._contract = self._get_contract(web3, self.abi, address)

    @staticmethod
    def deploy(web3: Web3, tub: Address, pit: Address):
        assert (isinstance(tub, Address))
        assert (isinstance(pit, Address))
        return Tap(web3=web3,
                   address=Contract._deploy(web3, Tap.abi, Tap.bin,
                                            [tub.address, pit.address]))

    def set_authority(self, address: Address) -> Transact:
        assert (isinstance(address, Address))
        return Transact(self, self.web3, self.abi, self.address,
                        self._contract, 'setAuthority', [address.address])

    def woe(self) -> Wad:
        """Get the amount of bad debt.

        Returns:
            The amount of bad debt in SAI.
        """
        return Wad(self._contract.call().woe())

    def fog(self) -> Wad:
        """Get the amount of SKR pending liquidation.

        Returns:
            The amount of SKR pending liquidation, in SKR.
        """
        return Wad(self._contract.call().fog())

    #TODO beware that it doesn't call drip() underneath so if `tax`>1.0 we won't get an up-to-date value of joy()
    def joy(self) -> Wad:
        """Get the amount of surplus SAI.

        Surplus SAI can be processed using `boom()`.

        Returns:
            The amount of surplus SAI accumulated in the Tub.
        """
        return Wad(self._contract.call().joy())

    def gap(self) -> Wad:
        """Get the current spread for `boom` and `bust`.

        Returns:
            The current spread for `boom` and `bust`. `1.0` means no spread, `1.01` means 1% spread.
        """
        return Wad(self._contract.call().gap())

    def jump(self, new_gap: Wad) -> Transact:
        """Update the current spread (`gap`) for `boom` and `bust`.

        Args:
            new_gap: The new value of the spread (`gap`). `1.0` means no spread, `1.01` means 1% spread.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert isinstance(new_gap, Wad)
        return Transact(self, self.web3, self.abi, self.address,
                        self._contract, 'jump', [new_gap.value])

    def s2s(self) -> Wad:
        """Get the current SKR per SAI rate (for `boom` and `bust`).

        Returns:
            The current SKR per SAI rate.
        """
        return Wad(self._contract.call().s2s())

    def bid(self) -> Wad:
        """Get the current price of SKR in SAI for `boom`.

        Returns:
            The SKR in SAI price that will be used on `boom()`.
        """
        return Wad(self._contract.call().bid())

    def ask(self) -> Wad:
        """Get the current price of SKR in SAI for `bust`.

        Returns:
            The SKR in SAI price that will be used on `bust()`.
        """
        return Wad(self._contract.call().ask())

    def boom(self, amount_in_skr: Wad) -> Transact:
        """Buy some amount of SAI to process `joy` (surplus).

        Args:
            amount_in_skr: The amount of SKR we want to send in order to receive SAI.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert isinstance(amount_in_skr, Wad)
        return Transact(self, self.web3, self.abi, self.address,
                        self._contract, 'boom', [amount_in_skr.value])

    def bust(self, amount_in_skr: Wad) -> Transact:
        """Sell some amount of SAI to process `woe` (bad debt).

        Args:
            amount_in_skr: The amount of SKR we want to receive in exchange for our SAI.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert isinstance(amount_in_skr, Wad)
        return Transact(self, self.web3, self.abi, self.address,
                        self._contract, 'bust', [amount_in_skr.value])

    def __eq__(self, other):
        assert (isinstance(other, Tap))
        return self.address == other.address

    def __repr__(self):
        return f"Tap('{self.address}')"
Exemple #9
0
class Tub(Contract):
    """A client for the `Tub` contract, the primary contract driving the `SAI Stablecoin System`.

    SAI is a simple version of the diversely collateralized DAI stablecoin.

    In this model there is one type of underlying collateral (called gems).
    The SKR token represents claims on the system's excess gems, and is the
    only admissible type of collateral.  Gems can be converted to/from SKR.

    Any transfers of SAI or SKR are done using the normal ERC20 interface;
    until settlement mode is triggered, SAI users should only need ERC20.
    ``ERC20Token`` class may be used for it.

    Attributes:
        web3: An instance of `Web` from `web3.py`.
        address: Ethereum address of the `Tub` contract.
    """

    abiTub = Contract._load_abi(__name__, 'abi/Tub.abi')
    binTub = Contract._load_bin(__name__, 'abi/Tub.bin')
    abiTip = Contract._load_abi(__name__, 'abi/Tip.abi')
    abiJar = Contract._load_abi(__name__, 'abi/SaiJar.abi')
    abiJug = Contract._load_abi(__name__, 'abi/SaiJug.abi')

    def __init__(self, web3: Web3, address: Address):
        self.web3 = web3
        self.address = address
        self._contractTub = self._get_contract(web3, self.abiTub, address)
        self._contractTip = self._get_contract(
            web3, self.abiTip, Address(self._contractTub.call().tip()))
        self._contractJar = self._get_contract(
            web3, self.abiJar, Address(self._contractTub.call().jar()))
        self._contractJug = self._get_contract(
            web3, self.abiJug, Address(self._contractTub.call().jug()))

    @staticmethod
    def deploy(web3: Web3, jar: Address, jug: Address, pot: Address,
               pit: Address, tip: Address):
        assert (isinstance(jar, Address))
        assert (isinstance(jug, Address))
        assert (isinstance(pot, Address))
        assert (isinstance(pit, Address))
        assert (isinstance(tip, Address))
        return Tub(
            web3=web3,
            address=Contract._deploy(web3, Tub.abiTub, Tub.binTub, [
                jar.address, jug.address, pot.address, pit.address, tip.address
            ]))

    def set_authority(self, address: Address):
        assert (isinstance(address, Address))
        Transact(self, self.web3, self.abiTub, self.address, self._contractTub,
                 'setAuthority', [address.address]).transact()
        Transact(self, self.web3, self.abiTip, self.tip(), self._contractTip,
                 'setAuthority', [address.address]).transact()
        Transact(self, self.web3, self.abiJar, self.jar(), self._contractJar,
                 'setAuthority', [address.address]).transact()
        Transact(self, self.web3, self.abiJug, self.jug(), self._contractJug,
                 'setAuthority', [address.address]).transact()

    def approve(self, approval_function):
        approval_function(ERC20Token(web3=self.web3, address=self.gem()),
                          self.jar(), 'Tub.jar')
        approval_function(ERC20Token(web3=self.web3, address=self.skr()),
                          self.jar(), 'Tub.jar')
        approval_function(ERC20Token(web3=self.web3, address=self.sai()),
                          self.pot(), 'Tub.pot')
        approval_function(ERC20Token(web3=self.web3, address=self.skr()),
                          self.pit(), 'Tub.pit')
        approval_function(ERC20Token(web3=self.web3, address=self.sai()),
                          self.pit(), 'Tub.pit')

    def era(self) -> int:
        """Return the current SAI contracts timestamp.

        Returns:
            Timestamp as a unix timestamp.
        """
        return self._contractTip.call().era()

    def warp(self, seconds: int) -> Transact:
        """Move the SAI contracts forward in time.

        If the `seconds` parameter is equal to `0`, time travel will get permanently disabled
        and subsequent `warp()` calls will always fail.

        Args:
            seconds: Number of seconds to warp the time forward, or `0` to permanently disable time travel.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        return Transact(self, self.web3, self.abiTip, self.tip(),
                        self._contractTip, 'warp', [seconds])

    def sai(self) -> Address:
        """Get the SAI token.

        Returns:
            The address of the SAI token.
        """
        return Address(self._contractTub.call().sai())

    def sin(self) -> Address:
        """Get the SIN token.

        Returns:
            The address of the SIN token.
        """
        return Address(self._contractTub.call().sin())

    def jug(self) -> Address:
        """Get the SAI/SIN tracker.

        Returns:
            The address of the SAI/SIN tracker token.
        """
        return Address(self._contractTub.call().jug())

    def jar(self) -> Address:
        """Get the collateral vault.

        Returns:
            The address of the `SaiJar` vault. It is an internal component of Sai.
        """
        return Address(self._contractTub.call().jar())

    def pit(self) -> Address:
        """Get the liquidator vault.

        Returns:
            The address of the `DSVault` holding the bad debt.
        """
        return Address(self._contractTub.call().pit())

    def pot(self) -> Address:
        """Get the good debt vault.

        Returns:
            The address of the `DSVault` holding the good debt.
        """
        return Address(self._contractTub.call().pot())

    def skr(self) -> Address:
        """Get the SKR token.

        Returns:
            The address of the SKR token.
        """
        return Address(self._contractTub.call().skr())

    def gem(self) -> Address:
        """Get the collateral token (eg. W-ETH).

        Returns:
            The address of the collateral token.
        """
        return Address(self._contractTub.call().gem())

    def pip(self) -> Address:
        """Get the GEM price feed.

        Returns:
            The address of the GEM price feed, which could be a `DSValue`, a `DSCache`, a `Mednianizer` etc.
        """
        return Address(self._contractJar.call().pip())

    def tip(self) -> Address:
        """Get the target price engine.

        Returns:
            The address of the target price engine. It is an internal component of Sai.
        """
        return Address(self._contractTub.call().tip())

    def axe(self) -> Ray:
        """Get the liquidation penalty.

        Returns:
            The liquidation penalty. `1.0` means no penalty. `1.2` means 20% penalty.
        """
        return Ray(self._contractTub.call().axe())

    def hat(self) -> Wad:
        """Get the debt ceiling.

        Returns:
            The debt ceiling in SAI.
        """
        return Wad(self._contractTub.call().hat())

    def mat(self) -> Ray:
        """Get the liquidation ratio.

        Returns:
            The liquidation ratio. `1.5` means the liquidation ratio is 150%.
        """
        return Ray(self._contractTub.call().mat())

    def tax(self) -> Ray:
        """Get the stability fee.

        Returns:
            Per-second value of the stability fee. `1.0` means no stability fee.
        """
        return Ray(self._contractTub.call().tax())

    def way(self) -> Ray:
        """Get the holder fee (interest rate).

        Returns:
            Per-second value of the holder fee. `1.0` means no holder fee.
        """
        return Ray(self._contractTip.call().way())

    def reg(self) -> int:
        """Get the Tub stage ('register').

        Returns:
            The current Tub stage (0=Usual, 1=Caged).
        """
        return self._contractTub.call().reg()

    def fit(self) -> Ray:
        """Get the GEM per SKR settlement price.

        Returns:
            The GEM per SKR settlement (kill) price.
        """
        return Ray(self._contractTub.call().fit())

    def rho(self) -> int:
        """Get the time of the last drip.

        Returns:
            The time of the last drip as a unix timestamp.
        """
        return self._contractTub.call().rho()

    def tau(self) -> int:
        """Get the time of the last prod.

        Returns:
            The time of the last prod as a unix timestamp.
        """
        return self._contractTip.call().tau()

    def chi(self) -> Ray:
        """Get the internal debt price.

        Every invocation of this method calls `drip()` internally, so the value you receive is always up-to-date.
        But as calling it doesn't result in an Ethereum transaction, the actual `_chi` value in the smart
        contract storage does not get updated.

        Returns:
            The internal debt price in SAI.
        """
        return Ray(self._contractTub.call().chi())

    def chop(self, new_axe: Ray) -> Transact:
        """Update the liquidation penalty.

        Args:
            new_axe: The new value of the liquidation penalty (`axe`). `1.0` means no penalty. `1.2` means 20% penalty.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert isinstance(new_axe, Ray)
        return Transact(self, self.web3, self.abiTub, self.address,
                        self._contractTub, 'chop', [new_axe.value])

    def cork(self, new_hat: Wad) -> Transact:
        """Update the debt ceiling.

        Args:
            new_hat: The new value of the debt ceiling (`hat`), in SAI.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert isinstance(new_hat, Wad)
        return Transact(self, self.web3, self.abiTub, self.address,
                        self._contractTub, 'cork', [new_hat.value])

    def cuff(self, new_mat: Ray) -> Transact:
        """Update the liquidation ratio.

        Args:
            new_mat: The new value of the liquidation ratio (`mat`). `1.5` means the liquidation ratio is 150%.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert isinstance(new_mat, Ray)
        return Transact(self, self.web3, self.abiTub, self.address,
                        self._contractTub, 'cuff', [new_mat.value])

    def crop(self, new_tax: Ray) -> Transact:
        """Update the stability fee.

        Args:
            new_tax: The new per-second value of the stability fee (`tax`). `1.0` means no stability fee.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert isinstance(new_tax, Ray)
        return Transact(self, self.web3, self.abiTub, self.address,
                        self._contractTub, 'crop', [new_tax.value])

    def coax(self, new_way: Ray) -> Transact:
        """Update the holder fee.

        Args:
            new_way: The new per-second value of the holder fee (`way`). `1.0` means no holder fee.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert isinstance(new_way, Ray)
        return Transact(self, self.web3, self.abiTip, self.tip(),
                        self._contractTip, 'coax', [new_way.value])

    def drip(self) -> Transact:
        """Recalculate the internal debt price (`chi`).

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        return Transact(self, self.web3, self.abiTub, self.address,
                        self._contractTub, 'drip', [])

    def prod(self) -> Transact:
        """Recalculate the accrued holder fee (`par`).

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        return Transact(self, self.web3, self.abiTip, self.tip(),
                        self._contractTip, 'prod', [])

    def ice(self) -> Wad:
        """Get the amount of good debt.

        Returns:
            The amount of good debt in SAI.
        """
        return Wad(self._contractTub.call().ice())

    def pie(self) -> Wad:
        """Get the amount of raw collateral.

        Returns:
            The amount of raw collateral in GEM.
        """
        return Wad(self._contractTub.call().pie())

    def air(self) -> Wad:
        """Get the amount of backing collateral.

        Returns:
            The amount of backing collateral in SKR.
        """
        return Wad(self._contractTub.call().air())

    def tag(self) -> Wad:
        """Get the reference price (REF per SKR).

        The price is read from the price feed (`tip()`) every time this method gets called.
        Its value is actually the value from the feed (REF per GEM) multiplied by `per()` (GEM per SKR).

        Returns:
            The reference price (REF per SKR).
        """
        return Wad(self._contractJar.call().tag())

    def par(self) -> Wad:
        """Get the accrued holder fee (REF per SAI).

        Every invocation of this method calls `prod()` internally, so the value you receive is always up-to-date.
        But as calling it doesn't result in an Ethereum transaction, the actual `_par` value in the smart
        contract storage does not get updated.

        Returns:
            The accrued holder fee.
        """
        return Wad(self._contractTip.call().par())

    def per(self) -> Ray:
        """Get the current average entry/exit price (GEM per SKR).

        In order to get the price that will be actually used on `join()` or `exit()`, see
        `jar_ask()` and `jar_bid()` respectively.

        Returns:
            The current GEM per SKR price.
        """
        return Ray(self._contractJar.call().per())

    # TODO these prefixed methods are ugly, the ultimate solution would be to have a class per smart contract
    def jar_gap(self) -> Wad:
        """Get the current spread for `join` and `exit`.

        Returns:
            The current spread for `join` and `exit`. `1.0` means no spread, `1.01` means 1% spread.
        """
        return Wad(self._contractJar.call().gap())

    # TODO these prefixed methods are ugly, the ultimate solution would be to have a class per smart contract
    def jar_jump(self, new_gap: Wad) -> Transact:
        """Update the current spread (`gap`) for `join` and `exit`.

        Args:
            new_tax: The new value of the spread (`gap`). `1.0` means no spread, `1.01` means 1% spread.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert isinstance(new_gap, Wad)
        return Transact(self, self.web3, self.abiJar, self.jar(),
                        self._contractJar, 'jump', [new_gap.value])

    # TODO these prefixed methods are ugly, the ultimate solution would be to have a class per smart contract
    def jar_bid(self) -> Ray:
        """Get the current `exit()` price (GEM per SKR).

        Returns:
            The GEM per SKR price that will be used on `exit()`.
        """
        return Ray(self._contractJar.call().bid())

    # TODO these prefixed methods are ugly, the ultimate solution would be to have a class per smart contract
    def jar_ask(self) -> Ray:
        """Get the current `join()` price (GEM per SKR).

        Returns:
            The GEM per SKR price that will be used on `join()`.
        """
        return Ray(self._contractJar.call().ask())

    def cupi(self) -> int:
        """Get the last cup id

        Returns:
            The id of the last cup created. Zero if no cups have been created so far.
        """
        return self._contractTub.call().cupi()

    def cups(self, cup_id: int) -> Cup:
        """Get the cup details.

        Args:
            cup_id: Id of the cup to get the details of.

        Returns:
            Class encapsulating cup details.
        """
        assert isinstance(cup_id, int)
        array = self._contractTub.call().cups(int_to_bytes32(cup_id))
        return Cup(cup_id, Address(array[0]), Wad(array[1]), Wad(array[2]))

    def tab(self, cup_id: int) -> Wad:
        """Get the amount of debt in a cup.

        Args:
            cup_id: Id of the cup.

        Returns:
            Amount of debt in the cup, in SAI.
        """
        assert isinstance(cup_id, int)
        return Wad(self._contractTub.call().tab(int_to_bytes32(cup_id)))

    def ink(self, cup_id: int) -> Wad:
        """Get the amount of SKR collateral locked in a cup.

        Args:
            cup_id: Id of the cup.

        Returns:
            Amount of SKR collateral locked in the cup, in SKR.
        """
        assert isinstance(cup_id, int)
        return Wad(self._contractTub.call().ink(int_to_bytes32(cup_id)))

    def lad(self, cup_id: int) -> Address:
        """Get the owner of a cup.

        Args:
            cup_id: Id of the cup.

        Returns:
            Address of the owner of the cup.
        """
        assert isinstance(cup_id, int)
        return Address(self._contractTub.call().lad(int_to_bytes32(cup_id)))

    def safe(self, cup_id: int) -> bool:
        """Determine if a cup is safe.

        Args:
            cup_id: Id of the cup

        Returns:
            `True` if the cup is safe. `False` otherwise.
        """
        assert isinstance(cup_id, int)
        return self._contractTub.call().safe(int_to_bytes32(cup_id))

    def join(self, amount_in_gem: Wad) -> Transact:
        """Buy SKR for GEMs.

        Args:
            amount_in_gem: The amount of GEMs to buy SKR for.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert isinstance(amount_in_gem, Wad)
        return Transact(self, self.web3, self.abiTub, self.address,
                        self._contractTub, 'join', [amount_in_gem.value])

    def exit(self, amount_in_skr: Wad) -> Transact:
        """Sell SKR for GEMs.

        Args:
            amount_in_skr: The amount of SKR to sell for GEMs.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert isinstance(amount_in_skr, Wad)
        return Transact(self, self.web3, self.abiTub, self.address,
                        self._contractTub, 'exit', [amount_in_skr.value])

    #TODO make it return the id of the newly created cup
    def open(self) -> Transact:
        """Create a new cup.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        return Transact(self, self.web3, self.abiTub, self.address,
                        self._contractTub, 'open', [])

    def shut(self, cup_id: int) -> Transact:
        """Close a cup.

        Involves calling `wipe()` and `free()` internally in order to clear all remaining SAI debt and free
        all remaining SKR collateral.

        Args:
            cup_id: Id of the cup to close.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert isinstance(cup_id, int)
        return Transact(self, self.web3, self.abiTub, self.address,
                        self._contractTub, 'shut', [int_to_bytes32(cup_id)])

    def lock(self, cup_id: int, amount_in_skr: Wad) -> Transact:
        """Post additional SKR collateral to a cup.

        Args:
            cup_id: Id of the cup to post the collateral into.
            amount_in_skr: The amount of collateral to post, in SKR.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert isinstance(cup_id, int)
        assert isinstance(amount_in_skr, Wad)
        return Transact(self, self.web3, self.abiTub, self.address,
                        self._contractTub, 'lock',
                        [int_to_bytes32(cup_id), amount_in_skr.value])

    def free(self, cup_id: int, amount_in_skr: Wad) -> Transact:
        """Remove excess SKR collateral from a cup.

        Args:
            cup_id: Id of the cup to remove the collateral from.
            amount_in_skr: The amount of collateral to remove, in SKR.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert isinstance(cup_id, int)
        assert isinstance(amount_in_skr, Wad)
        return Transact(self, self.web3, self.abiTub, self.address,
                        self._contractTub, 'free',
                        [int_to_bytes32(cup_id), amount_in_skr.value])

    def draw(self, cup_id: int, amount_in_sai: Wad) -> Transact:
        """Issue the specified amount of SAI stablecoins.

        Args:
            cup_id: Id of the cup to issue the SAI from.
            amount_in_sai: The amount SAI to be issued.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert isinstance(cup_id, int)
        assert isinstance(amount_in_sai, Wad)
        return Transact(self, self.web3, self.abiTub, self.address,
                        self._contractTub, 'draw',
                        [int_to_bytes32(cup_id), amount_in_sai.value])

    def wipe(self, cup_id: int, amount_in_sai: Wad) -> Transact:
        """Repay some portion of existing SAI debt.

        Args:
            cup_id: Id of the cup to repay the SAI to.
            amount_in_sai: The amount SAI to be repaid.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert isinstance(cup_id, int)
        assert isinstance(amount_in_sai, Wad)
        return Transact(self, self.web3, self.abiTub, self.address,
                        self._contractTub, 'wipe',
                        [int_to_bytes32(cup_id), amount_in_sai.value])

    def give(self, cup_id: int, new_lad: Address) -> Transact:
        """Transfer ownership of a cup.

        Args:
            cup_id: Id of the cup to transfer ownership of.
            new_lad: New owner of the cup.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert isinstance(cup_id, int)
        assert isinstance(new_lad, Address)
        return Transact(self, self.web3, self.abiTub, self.address,
                        self._contractTub, 'give',
                        [int_to_bytes32(cup_id), new_lad.address])

    def bite(self, cup_id: int) -> Transact:
        """Initiate liquidation of an undercollateralized cup.

        Args:
            cup_id: Id of the cup to liquidate.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert isinstance(cup_id, int)
        return Transact(self, self.web3, self.abiTub, self.address,
                        self._contractTub, 'bite', [int_to_bytes32(cup_id)])

    def __eq__(self, other):
        assert (isinstance(other, Tub))
        return self.address == other.addressTub

    def __repr__(self):
        return f"Tub('{self.address}')"
Exemple #10
0
class MatchingMarket(ExpiringMarket):
    """A client for a `MatchingMarket` contract.

    You can find the source code of the `OasisDEX` contracts here:
    <https://github.com/makerdao/maker-otc>.

    Attributes:
        web3: An instance of `Web` from `web3.py`.
        address: Ethereum address of the `MatchingMarket` contract.
    """

    abi = Contract._load_abi(__name__, 'abi/MatchingMarket.abi')
    bin = Contract._load_bin(__name__, 'abi/MatchingMarket.bin')

    @staticmethod
    def deploy(web3: Web3, close_time: int):
        """Deploy a new instance of the `MatchingMarket` contract.

        Args:
            web3: An instance of `Web` from `web3.py`.
            close_time: Unix timestamp of when the market will close.

        Returns:
            A `MatchingMarket` class instance.
        """
        return MatchingMarket(web3=web3,
                              address=Contract._deploy(web3,
                                                       MatchingMarket.abi,
                                                       MatchingMarket.bin,
                                                       [close_time]))

    def set_buy_enabled(self, buy_enabled: bool) -> Transact:
        """Enables or disables direct buy.

        Args:
            buy_enabled: Whether direct buy should be enabled or disabled.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert (isinstance(buy_enabled, bool))
        return Transact(self, self.web3, self.abi, self.address,
                        self._contract, 'setBuyEnabled', [buy_enabled])

    def set_matching_enabled(self, matching_enabled: bool) -> Transact:
        """Enables or disables order matching.

        Args:
            matching_enabled: Whether order matching should be enabled or disabled.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert (isinstance(matching_enabled, bool))
        return Transact(self, self.web3, self.abi, self.address,
                        self._contract, 'setMatchingEnabled',
                        [matching_enabled])

    def add_token_pair_whitelist(self, base_token: Address,
                                 quote_token: Address) -> Transact:
        """Adds a token pair to the whitelist.

        All incoming offers are checked against the whitelist.

        Args:
            base_token: Address of the ERC20 token.
            quote_token: Address of the ERC20 token.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert (isinstance(base_token, Address))
        assert (isinstance(quote_token, Address))

        return Transact(self, self.web3, self.abi, self.address,
                        self._contract, 'addTokenPairWhitelist',
                        [base_token.address, quote_token.address])

    def make(self,
             have_token: Address,
             have_amount: Wad,
             want_token: Address,
             want_amount: Wad,
             pos: int = None) -> Transact:
        """Create a new offer.

        The `have_amount` of `have_token` token will be taken from you on offer creation and deposited
        in the market contract. Allowance needs to be set first. Refer to the `approve()` method
        in the `ERC20Token` class.

        The `MatchingMarket` contract maintains an internal ordered linked list of offers, which allows the contract
        to do automated matching. Client placing a new offer can either let the contract find the correct
        position in the linked list (by passing `0` as the `pos` argument of `make`) or calculate the position
        itself and just pass the right value to the contract (this will happen if you omit the `pos`
        argument of `make`). The latter should always use less gas. If the client decides not to calculate the
        position or it does get it wrong and the number of open orders is high at the same time, the new offer
        may not even be placed at all as the attempt to calculate the position by the contract will likely fail
        due to high gas usage.

        Args:
            have_token: Address of the ERC20 token you want to put on sale.
            have_amount: Amount of the `have_token` token you want to put on sale.
            want_token: Address of the ERC20 token you want to be paid with.
            want_amount: Amount of the `want_token` you want to receive.
            pos: The position to insert the order at in the sorted list.
                If `None`, the optimal position will automatically get calculated.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert (isinstance(have_token, Address))
        assert (isinstance(have_amount, Wad))
        assert (isinstance(want_token, Address))
        assert (isinstance(want_amount, Wad))
        assert (isinstance(pos, int) or (pos is None))
        assert (have_amount > Wad(0))
        assert (want_amount > Wad(0))

        if pos is None:
            pos = self.position(have_token=have_token,
                                have_amount=have_amount,
                                want_token=want_token,
                                want_amount=want_amount)
        else:
            assert (pos >= 0)

        return Transact(self, self.web3, self.abi, self.address,
                        self._contract, 'offer', [
                            have_amount.value, have_token.address,
                            want_amount.value, want_token.address, pos
                        ])

    def position(self, have_token: Address, have_amount: Wad,
                 want_token: Address, want_amount: Wad) -> int:
        """Calculate the position (`pos`) new offer should be inserted at to minimize gas costs.

        The `MatchingMarket` contract maintains an internal ordered linked list of offers, which allows the contract
        to do automated matching. Client placing a new offer can either let the contract find the correct
        position in the linked list (by passing `0` as the `pos` argument of `make`) or calculate the position
        itself and just pass the right value to the contract (this will happen if you omit the `pos`
        argument of `make`). The latter should always use less gas. If the client decides not to calculate the
        position or it does get it wrong and the number of open orders is high at the same time, the new offer
        may not even be placed at all as the attempt to calculate the position by the contract will likely fail
        due to high gas usage.

        This method is responsible for calculating the correct insertion position. It is used internally
        by `make` when `pos` argument is omitted (or is `None`).

        Args:
            have_token: Address of the ERC20 token you want to put on sale.
            have_amount: Amount of the `have_token` token you want to put on sale.
            want_token: Address of the ERC20 token you want to be paid with.
            want_amount: Amount of the `want_token` you want to receive.

        Returns:
            The position (`pos`) new offer should be inserted at.
        """
        assert (isinstance(have_token, Address))
        assert (isinstance(have_amount, Wad))
        assert (isinstance(want_token, Address))
        assert (isinstance(want_amount, Wad))

        offers = filter(
            lambda o: o.sell_which_token == have_token and o.buy_which_token ==
            want_token and o.sell_how_much / o.buy_how_much >= have_amount /
            want_amount, self.active_offers())

        sorted_offers = sorted(offers,
                               key=lambda o: o.sell_how_much / o.buy_how_much)
        return sorted_offers[0].offer_id if len(sorted_offers) > 0 else 0

    def __repr__(self):
        return f"MatchingMarket('{self.address}')"
Exemple #11
0
class SimpleMarket(Contract):
    """A client for a `SimpleMarket` contract.

    `SimpleMarket` is a simple on-chain OTC market for ERC20-compatible tokens.
    It powers the `OasisDEX` decentralized exchange.

    You can find the source code of the `OasisDEX` contracts here:
    <https://github.com/makerdao/maker-otc>.

    Attributes:
        web3: An instance of `Web` from `web3.py`.
        address: Ethereum address of the `SimpleMarket` contract.
    """

    abi = Contract._load_abi(__name__, 'abi/SimpleMarket.abi')
    bin = Contract._load_bin(__name__, 'abi/SimpleMarket.bin')

    def __init__(self, web3: Web3, address: Address):
        self.web3 = web3
        self.address = address
        self._contract = self._get_contract(web3, self.abi, address)
        self._none_offers = set()

    @staticmethod
    def deploy(web3: Web3):
        """Deploy a new instance of the `SimpleMarket` contract.

        Args:
            web3: An instance of `Web` from `web3.py`.

        Returns:
            A `SimpleMarket` class instance.
        """
        return SimpleMarket(web3=web3,
                            address=Contract._deploy(web3, SimpleMarket.abi,
                                                     SimpleMarket.bin, []))

    def approve(self, tokens: List[ERC20Token], approval_function):
        for token in tokens:
            approval_function(token, self.address, 'OasisDEX')

    def on_make(self, handler):
        self._on_event(self._contract, 'LogMake', LogMake, handler)

    def on_bump(self, handler):
        self._on_event(self._contract, 'LogBump', LogBump, handler)

    def on_take(self, handler):
        self._on_event(self._contract, 'LogTake', LogTake, handler)

    def on_kill(self, handler):
        self._on_event(self._contract, 'LogKill', LogKill, handler)

    def past_make(self, number_of_past_blocks: int) -> List[LogMake]:
        return self._past_events(self._contract, 'LogMake', LogMake,
                                 number_of_past_blocks)

    def past_bump(self, number_of_past_blocks: int) -> List[LogBump]:
        return self._past_events(self._contract, 'LogBump', LogBump,
                                 number_of_past_blocks)

    def past_take(self, number_of_past_blocks: int) -> List[LogTake]:
        return self._past_events(self._contract, 'LogTake', LogTake,
                                 number_of_past_blocks)

    def past_kill(self, number_of_past_blocks: int) -> List[LogKill]:
        return self._past_events(self._contract, 'LogKill', LogKill,
                                 number_of_past_blocks)

    def get_last_offer_id(self) -> int:
        """Get the id of the last offer created on the market.

        Returns:
            The id of the last offer. Returns `0` if no offers have been created at all.
        """
        return self._contract.call().last_offer_id()

    def get_offer(self, offer_id: int) -> Optional[OfferInfo]:
        """Get the offer details.

        Args:
            offer_id: The id of the offer to get the details of.

        Returns:
            An instance of `OfferInfo` if the offer is still active, or `None` if the offer has been
            already completely taken.
        """

        # if an offer is None, it won't become not-None again for the same OTC instance
        if offer_id in self._none_offers:
            return None

        array = self._contract.call().offers(offer_id)
        if array[5] is not True:
            self._none_offers.add(offer_id)
            return None
        else:
            return OfferInfo(offer_id=offer_id,
                             sell_how_much=Wad(array[0]),
                             sell_which_token=Address(array[1]),
                             buy_how_much=Wad(array[2]),
                             buy_which_token=Address(array[3]),
                             owner=Address(array[4]),
                             timestamp=array[6])

    def active_offers(self) -> List[OfferInfo]:
        offers = [
            self.get_offer(offer_id + 1)
            for offer_id in range(self.get_last_offer_id())
        ]
        return [offer for offer in offers if offer is not None]

    #TODO make it return the id of the newly created offer
    def make(self, have_token: Address, have_amount: Wad, want_token: Address,
             want_amount: Wad) -> Transact:
        """Create a new offer.

        The `have_amount` of `have_token` token will be taken from you on offer creation and deposited
        in the market contract. Allowance needs to be set first. Refer to the `approve()` method
        in the `ERC20Token` class.

        Args:
            have_token: Address of the ERC20 token you want to put on sale.
            have_amount: Amount of the `have_token` token you want to put on sale.
            want_token: Address of the ERC20 token you want to be paid with.
            want_amount: Amount of the `want_token` you want to receive.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        return Transact(self, self.web3, self.abi, self.address,
                        self._contract, 'make', [
                            have_token.address, want_token.address,
                            have_amount.value, want_amount.value
                        ])

    def take(self, offer_id: int, quantity: Wad) -> Transact:
        """Takes (buys) an offer.

        If `quantity` is equal to `sell_how_much`, the whole offer will be taken (bought) which will make it
        disappear from the order book. If you want to buy a fraction of the offer, set `quantity` to a number
        lower than `sell_how_much`.

        Args:
            offer_id: Id of the offer you want to take (buy).
            quantity: Quantity of `sell_which_token` that you want to buy.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        return Transact(self, self.web3, self.abi, self.address,
                        self._contract, 'take',
                        [int_to_bytes32(offer_id), quantity.value])

    def kill(self, offer_id: int) -> Transact:
        """Cancels an existing offer.

        Offers can be cancelled only by their owners. In addition to that, in case of expiring markets,
        after the market has expired all orders can be cancelled by anyone.

        Args:
            offer_id: Id of the offer you want to cancel.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        return Transact(self, self.web3, self.abi, self.address,
                        self._contract, 'kill', [int_to_bytes32(offer_id)])

    def __repr__(self):
        return f"SimpleMarket('{self.address}')"
Exemple #12
0
class DSEthToken(ERC20Token):
    """A client for the `DSEthToken` contract.

    `DSEthToken`, also known as ETH Wrapper or W-ETH, is a contract into which you can deposit
    raw ETH and then deal with it like with any other ERC20 token. In addition to the `deposit()`
    and `withdraw()` methods, it implements the standard ERC20 token API.

    You can find the source code of the `DSEthToken` contract here:
    <https://github.com/dapphub/ds-eth-token>.

    Attributes:
        web3: An instance of `Web` from `web3.py`.
        address: Ethereum address of the `DSEthToken` contract.
    """

    abi = Contract._load_abi(__name__, 'abi/DSEthToken.abi')
    bin = Contract._load_bin(__name__, 'abi/DSEthToken.bin')

    @staticmethod
    def deploy(web3: Web3):
        """Deploy a new instance of the `DSEthToken` contract.

        Args:
            web3: An instance of `Web` from `web3.py`.

        Returns:
            A `DSEthToken` class instance.
        """
        return DSEthToken(web3=web3, address=Contract._deploy(web3, DSEthToken.abi, DSEthToken.bin, []))

    def __init__(self, web3, address):
        super().__init__(web3, address)
        self._contract = self._get_contract(web3, self.abi, address)

    def deposit(self, amount: Wad) -> Transact:
        """Deposits `amount` of raw ETH to `DSEthToken`.

        Args:
            amount: Amount of raw ETH to be deposited to `DSEthToken`.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert(isinstance(amount, Wad))
        return Transact(self, self.web3, self.abi, self.address, self._contract, 'deposit', [], {'value': amount.value})

    def withdraw(self, amount: Wad) -> Transact:
        """Withdraws `amount` of raw ETH from `DSEthToken`.

        The withdrawn ETH will get transferred to the calling account.

        Args:
            amount: Amount of raw ETH to be withdrawn from `DSEthToken`.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert(isinstance(amount, Wad))
        return Transact(self, self.web3, self.abi, self.address, self._contract, 'withdraw', [amount.value])

    def __repr__(self):
        return f"DSEthToken('{self.address}')"
Exemple #13
0
class DSToken(ERC20Token):
    """A client for the `DSToken` contract.

    You can find the source code of the `DSToken` contract here:
    <https://github.com/dapphub/ds-token>.

    Attributes:
        web3: An instance of `Web` from `web3.py`.
        address: Ethereum address of the `DSToken` contract.
    """

    abi = Contract._load_abi(__name__, 'abi/DSToken.abi')
    bin = Contract._load_bin(__name__, 'abi/DSToken.bin')

    @staticmethod
    def deploy(web3: Web3, symbol: str):
        """Deploy a new instance of the `DSToken` contract.

        Args:
            web3: An instance of `Web` from `web3.py`.
            symbol: Symbol of the new token.

        Returns:
            A `DSToken` class instance.
        """
        assert(isinstance(symbol, str))
        return DSToken(web3=web3, address=Contract._deploy(web3, DSToken.abi, DSToken.bin, [symbol]))

    def authority(self) -> Address:
        """Return the current `authority` of a `DSAuth`-ed contract.

        Returns:
            The address of the current `authority`.
        """
        return Address(self._contract.call().authority())

    def set_authority(self, address: Address) -> Transact:
        """Set the `authority` of a `DSAuth`-ed contract.

        Args:
            address: The address of the new `authority`.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert(isinstance(address, Address))
        return Transact(self, self.web3, self.abi, self.address, self._contract, 'setAuthority', [address.address])

    def mint(self, amount: Wad) -> Transact:
        """Increase the total supply of the token.

        Args:
            amount: The amount to increase the total supply by.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert(isinstance(amount, Wad))
        return Transact(self, self.web3, self.abi, self.address, self._contract, 'mint', [amount.value])

    def burn(self, amount: Wad) -> Transact:
        """Decrease the total supply of the token.

        Args:
            amount: The amount to decrease the total supply by.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert(isinstance(amount, Wad))
        return Transact(self, self.web3, self.abi, self.address, self._contract, 'burn', [amount.value])

    def __repr__(self):
        return f"DSToken('{self.address}')"
Exemple #14
0
class DSValue(Contract):
    """A client for the `DSValue` contract, a single-value data feed.

    `DSValue` is a single-value data feed, which means it can be in one of two states.
    It can either contain a value (in which case `has_value()` returns `True` and the read methods
    return that value) or be empty (in which case `has_value()` returns `False` and the read
    methods throw exceptions).

    `DSValue` can be populated with a new value using `poke()` and cleared using `void()`.

    Everybody can read from a `DSValue`.
    Calling `poke()` and `void()` is usually whitelisted to some addresses only.

    The `DSValue` contract keeps the value as a 32-byte array (Ethereum `bytes32` type).
    Methods have been provided to cast it into `int`, read as hex etc.

    You can find the source code of the `DSValue` contract here:
    <https://github.com/dapphub/ds-value>.

    Attributes:
        web3: An instance of `Web` from `web3.py`.
        address: Ethereum address of the `DSValue` contract.
    """

    abi = Contract._load_abi(__name__, 'abi/DSValue.abi')
    bin = Contract._load_bin(__name__, 'abi/DSValue.bin')

    @staticmethod
    def deploy(web3: Web3):
        return DSValue(web3=web3,
                       address=Contract._deploy(web3, DSValue.abi, DSValue.bin,
                                                []))

    def __init__(self, web3: Web3, address: Address):
        self.web3 = web3
        self.address = address
        self._assert_contract_exists(web3, address)
        self._contract = web3.eth.contract(abi=self.abi)(
            address=address.address)

    def has_value(self) -> bool:
        """Checks whether this instance contains a value.

        Returns:
            `True` if this instance contains a value, which can be read. `False` otherwise.
        """
        return self._contract.call().peek()[1]

    def read(self) -> bytes:
        """Reads the current value from this instance as a byte array.

        If this instance does not contain a value, throws an exception.

        Returns:
            A 32-byte array with the current value of this instance.
        """
        read_value = self._contract.call().read()
        return array.array('B', [ord(x) for x in read_value]).tostring()

    def read_as_hex(self) -> str:
        """Reads the current value from this instance and converts it to a hex string.

        If this instance does not contain a value, throws an exception.

        Returns:
            A string with a hexadecimal representation of the current value of this instance.
        """
        return ''.join(hex(x)[2:].zfill(2) for x in self.read())

    def read_as_int(self) -> int:
        """Reads the current value from this instance and converts it to an int.

        If the value is actually a `Ray` or a `Wad`, you can convert it to one using `Ray(...)`
        or `Wad(...)`. Please see `Ray` or `Wad` for more details.

        If this instance does not contain a value, throws an exception.

        Returns:
            An integer representation of the current value of this instance.
        """
        return int(self.read_as_hex(), 16)

    def poke(self, new_value: bytes) -> Transact:
        """Populates this instance with a new value.

        Args:
            new_value: A 32-byte array with the new value to be set.

        Returns:
            A `Transact` instance, which can be used to trigger the transaction.
        """
        assert (isinstance(new_value, bytes))
        assert (len(new_value) == 32)
        return Transact(self, self.web3, self.abi, self.address,
                        self._contract, 'poke', [new_value])

    def poke_with_int(self, new_value: int) -> Transact:
        """Populates this instance with a new value.

        Handles the conversion of a Python `int` into the Solidity `bytes32` type automatically.

        If the value you want to set is actually a `Ray` or a `Wad`, you can get the integer value from them
        by accessing their `value` property. Please see `Ray` or `Wad` for more details.

        Args:
            new_value: A non-negative integer with the new value to be set.

        Returns:
            A `Transact` instance, which can be used to trigger the transaction.
        """
        assert (isinstance(new_value, int))
        assert (new_value >= 0)
        return self.poke(new_value.to_bytes(32, byteorder='big'))

    def void(self) -> Transact:
        """Removes the current value from this instance.

        Returns:
            A `Transact` instance, which can be used to trigger the transaction.
        """
        return Transact(self, self.web3, self.abi, self.address,
                        self._contract, 'void', [])

    def __repr__(self):
        return f"DSValue('{self.address}')"
Exemple #15
0
class EtherDelta(Contract):
    """A client for the EtherDelta exchange contract.

    You can find the source code of the `EtherDelta` contract here:
    <https://etherscan.io/address/0x8d12a197cb00d4747a1fe03395095ce2a5cc6819#code>.

    Attributes:
        web3: An instance of `Web` from `web3.py`.
        address: Ethereum address of the `EtherDelta` contract.
        api_server: Base URL of the `EtherDelta` API server (for off-chain order support etc.).
            `None` if no off-chain order support desired.
    """

    abi = Contract._load_abi(__name__, 'abi/EtherDelta.abi')
    bin = Contract._load_bin(__name__, 'abi/EtherDelta.bin')

    ETH_TOKEN = Address('0x0000000000000000000000000000000000000000')

    @staticmethod
    def deploy(web3: Web3,
               admin: Address,
               fee_account: Address,
               account_levels_addr: Address,
               fee_make: Wad,
               fee_take: Wad,
               fee_rebate: Wad,
               api_server: str):
        """Deploy a new instance of the `EtherDelta` contract.

        Args:
            web3: An instance of `Web` from `web3.py`.
            api_server: Base URL of the `EtherDelta` API server (for off-chain order support etc.).
                `None` if no off-chain order support desired.

        Returns:
            A `EtherDelta` class instance.
        """
        return EtherDelta(web3=web3,
                          address=Contract._deploy(web3, EtherDelta.abi, EtherDelta.bin, [
                              admin.address,
                              fee_account.address,
                              account_levels_addr.address,
                              fee_make.value,
                              fee_take.value,
                              fee_rebate.value
                          ]),
                          api_server=api_server)

    def __init__(self, web3: Web3, address: Address, api_server: str):
        assert(isinstance(address, Address))
        assert(isinstance(api_server, str) or api_server is None)

        self.web3 = web3
        self.address = address
        self.api_server = api_server
        self._contract = self._get_contract(web3, self.abi, address)
        self._onchain_orders = None
        self._offchain_orders = set()

    def supports_offchain_orders(self) -> bool:
        return self.api_server is not None

    def approve(self, tokens: List[ERC20Token], approval_function):
        for token in tokens:
            approval_function(token, self.address, 'EtherDelta')

    def on_order(self, handler):
        self._on_event(self._contract, 'Order', LogOrder, handler)

    def on_cancel(self, handler):
        self._on_event(self._contract, 'Cancel', LogCancel, handler)

    def past_order(self, number_of_past_blocks: int) -> List[LogOrder]:
        return self._past_events(self._contract, 'Order', LogOrder, number_of_past_blocks)

    def past_cancel(self, number_of_past_blocks: int) -> List[LogCancel]:
        return self._past_events(self._contract, 'Cancel', LogCancel, number_of_past_blocks)

    def admin(self) -> Address:
        """Returns the address of the admin account.

        Returns:
            The address of the admin account.
        """
        return Address(self._contract.call().admin())

    def fee_account(self) -> Address:
        """Returns the address of the fee account i.e. the account that receives all fees collected.

        Returns:
            The address of the fee account.
        """
        return Address(self._contract.call().feeAccount())

    def account_levels_addr(self) -> Address:
        """Returns the address of the AccountLevels contract.

        Returns:
            The address of the AccountLevels contract.
        """
        return Address(self._contract.call().accountLevelsAddr())

    def fee_make(self) -> Wad:
        return Wad(self._contract.call().feeMake())

    def fee_take(self) -> Wad:
        return Wad(self._contract.call().feeTake())

    def fee_rebate(self) -> Wad:
        return Wad(self._contract.call().feeRebate())

    def deposit(self, amount: Wad) -> Transact:
        """Deposits `amount` of raw ETH to EtherDelta.

        Args:
            amount: Amount of raw ETH to be deposited on EtherDelta.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert(isinstance(amount, Wad))
        return Transact(self, self.web3, self.abi, self.address, self._contract, 'deposit', [], {'value': amount.value})

    def withdraw(self, amount: Wad) -> Transact:
        """Withdraws `amount` of raw ETH from EtherDelta.

        The withdrawn ETH will get transferred to the calling account.

        Args:
            amount: Amount of raw ETH to be withdrawn from EtherDelta.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert(isinstance(amount, Wad))
        return Transact(self, self.web3, self.abi, self.address, self._contract, 'withdraw', [amount.value])

    def balance_of(self, user: Address) -> Wad:
        """Returns the amount of raw ETH deposited by the specified user.

        Args:
            user: Address of the user to check the balance of.

        Returns:
            The raw ETH balance kept in the EtherDelta contract by the specified user.
        """
        assert(isinstance(user, Address))
        return Wad(self._contract.call().balanceOf('0x0000000000000000000000000000000000000000', user.address))

    def deposit_token(self, token: Address, amount: Wad) -> Transact:
        """Deposits `amount` of ERC20 token `token` to EtherDelta.

        Tokens will be pulled from the calling account, so the EtherDelta contract needs
        to have appropriate allowance. Either call `approve()` or set the allowance manually
        before trying to deposit tokens.

        Args:
            token: Address of the ERC20 token to be deposited.
            amount: Amount of token `token` to be deposited to EtherDelta.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert(isinstance(token, Address))
        assert(isinstance(amount, Wad))
        return Transact(self, self.web3, self.abi, self.address, self._contract, 'depositToken',
                        [token.address, amount.value])

    def withdraw_token(self, token: Address, amount: Wad) -> Transact:
        """Withdraws `amount` of ERC20 token `token` from EtherDelta.

        Tokens will get transferred to the calling account.

        Args:
            token: Address of the ERC20 token to be withdrawn.
            amount: Amount of token `token` to be withdrawn from EtherDelta.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert(isinstance(token, Address))
        assert(isinstance(amount, Wad))
        return Transact(self, self.web3, self.abi, self.address, self._contract, 'withdrawToken',
                        [token.address, amount.value])

    def balance_of_token(self, token: Address, user: Address) -> Wad:
        """Returns the amount of ERC20 token `token` deposited by the specified user.

        Args:
            token: Address of the ERC20 token return the balance of.
            user: Address of the user to check the balance of.

        Returns:
            The ERC20 token `token` balance kept in the EtherDelta contract by the specified user.
        """
        assert(isinstance(token, Address))
        assert(isinstance(user, Address))
        return Wad(self._contract.call().balanceOf(token.address, user.address))

    def active_onchain_orders(self) -> List[OnChainOrder]:
        # if this method is being called for the first time, discover existing orders
        # by looking for past events and set up monitoring of the future ones
        if self._onchain_orders is None:
            self._onchain_orders = set()
            self.on_order(lambda order: self._onchain_orders.add(order.to_order()))
            for old_order in self.past_order(1000000):
                self._onchain_orders.add(old_order.to_order())

        self._remove_filled_orders(self._onchain_orders)

        return list(self._onchain_orders)

    def active_offchain_orders(self, token1: Address, token2: Address) -> List[OffChainOrder]:
        assert(isinstance(token1, Address))
        assert(isinstance(token2, Address))

        if not self.supports_offchain_orders():
            raise Exception("Off-chain orders not supported for this EtherDelta instance")

        nonce = str(hash(token1.address)) + str(hash(token2.address)) + str(random.randint(1, 2**32 - 1))
        url = f"{self.api_server}/orders/{nonce}/{token1.address}/{token2.address}"
        res = requests.get(url)
        if res.ok:
            if len(res.text) > 0:
                orders_dicts = map(lambda entry: entry['order'], json.loads(res.text)['orders'])
                orders_dicts = filter(lambda order: Address(order['contractAddr']) == self.address, orders_dicts)
                orders = list(map(lambda order: OffChainOrder.from_json(order), orders_dicts))
                for order in orders:
                    self._offchain_orders.add(order)
        else:
            raise Exception("Fetch failed")

        self._remove_filled_orders(self._offchain_orders)

        return list(filter(lambda order: (order.token_get == token1 and order.token_give == token2) or
                                         (order.token_get == token2 and order.token_give == token1),
                           self._offchain_orders))

    def _remove_filled_orders(self, order_set: set):
        assert(isinstance(order_set, set))

        # remove orders which have been completely filled (or cancelled)
        for order in list(order_set):
            if self.amount_filled(order) == order.amount_get:
                order_set.remove(order)

    def place_order_onchain(self,
                            token_get: Address,
                            amount_get: Wad,
                            token_give: Address,
                            amount_give: Wad,
                            expires: int) -> Transact:
        """Creates a new on-chain order.

        Although it's not necessary to have any amount of `token_give` deposited to EtherDelta
        before placing an order, nobody will be able to take this order until some balance of
        'token_give' is provided.

        If you want to trade raw ETH, pass `Address('0x0000000000000000000000000000000000000000')`
        as either `token_get` or `token_give`.

        Args:
            token_get: Address of the ERC20 token you want to be paid with.
            amount_get:  Amount of the `token_get` you want to receive.
            token_give: Address of the ERC20 token you want to put on sale.
            amount_give: Amount of the `token_give` token you want to put on sale.
            expires: The block number after which the order will expire.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        nonce = self.random_nonce()
        result = Transact(self, self.web3, self.abi, self.address, self._contract, 'order',
                          [token_get.address, amount_get.value, token_give.address, amount_give.value, expires, nonce])

        # in order to avoid delay between order creation and the Order event,
        # which would cause `active_orders()` to return a stale list,
        # we add newly created order to that collection straight away
        #
        # as the collection is a set, if the event arrives later,
        # no duplicate will get added
        if self._onchain_orders is not None:
            onchain_order = OnChainOrder(token_get, amount_get, token_give, amount_give,
                                         expires, nonce, Address(self.web3.eth.defaultAccount))

            # TODO we shouldn't actually do it if we do not know the transaction went through
            self._onchain_orders.add(onchain_order)

        return result

    def place_order_offchain(self,
                             token_get: Address,
                             amount_get: Wad,
                             token_give: Address,
                             amount_give: Wad,
                             expires: int) -> Optional[OffChainOrder]:
        """Creates a new off-chain order.

        Although it's not necessary to have any amount of `token_give` deposited to EtherDelta
        before placing an order, nobody will be able to take this order until some balance of
        'token_give' is provided.

        If you want to trade raw ETH, pass `Address('0x0000000000000000000000000000000000000000')`
        as either `token_get` or `token_give`.

        Args:
            token_get: Address of the ERC20 token you want to be paid with.
            amount_get:  Amount of the `token_get` you want to receive.
            token_give: Address of the ERC20 token you want to put on sale.
            amount_give: Amount of the `token_give` token you want to put on sale.
            expires: The block number after which the order will expire.

        Returns:
            Newly created order as an instance of the `OffChainOrder` class.
        """

        def encode_address(address: Address) -> bytes:
            return get_single_encoder("address", None, None)(address.address)[12:]

        def encode_uint256(value: int) -> bytes:
            return get_single_encoder("uint", 256, None)(value)

        nonce = self.random_nonce()
        order_hash = hashlib.sha256(encode_address(self.address) +
                                    encode_address(token_get) +
                                    encode_uint256(amount_get.value) +
                                    encode_address(token_give) +
                                    encode_uint256(amount_give.value) +
                                    encode_uint256(expires) +
                                    encode_uint256(nonce)).digest()
        signed_hash = self._eth_sign(self.web3.eth.defaultAccount, order_hash)[2:]
        r = bytes.fromhex(signed_hash[0:64])
        s = bytes.fromhex(signed_hash[64:128])
        v = ord(bytes.fromhex(signed_hash[128:130]))

        off_chain_order = OffChainOrder(token_get, amount_get, token_give, amount_give, expires, nonce,
                                        Address(self.web3.eth.defaultAccount), v, r, s)

        if self.supports_offchain_orders():
            log_signature = f"('{token_get}', '{amount_get}', '{token_give}', '{amount_give}', '{expires}', '{nonce}')"

            try:
                self.logger.info(f"Creating off-chain EtherDelta order {log_signature} in progress...")
                self.logger.debug(json.dumps(off_chain_order.to_json(self.address)))
                res = requests.post(f"{self.api_server}/message",
                                    data={'message': json.dumps(off_chain_order.to_json(self.address))},
                                    timeout=30)

                if '"success"' in res.text:
                    self.logger.info(f"Created off-chain EtherDelta order {log_signature} successfully")
                    self._offchain_orders.add(off_chain_order)
                    return off_chain_order
                else:
                    self.logger.warning(f"Creating off-chain EtherDelta order {log_signature} failed ({res.text})")
                    return None
            except:
                self.logger.warning(f"Creating off-chain EtherDelta order {log_signature} failed ({sys.exc_info()[1]})")
                return None
        else:
            self.logger.warning(f"No EtherDelta API server configured, off-chain order has not been published")
            return off_chain_order

    def amount_available(self, order: Order) -> Wad:
        """Returns the amount that is still available (tradeable) for an order.

        The result will never be greater than `order.amount_get - amount_filled(order)`.
        It can be lower though if the order maker does not have enough balance on EtherDelta.

        Args:
            order: The order object you want to know the available amount of.
                Can be either an `OnChainOrder` or an `OffChainOrder`.

        Returns:
            The available amount for the order, in terms of `token_get`.
        """
        return Wad(self._contract.call().availableVolume(order.token_get.address,
                                                         order.amount_get.value,
                                                         order.token_give.address,
                                                         order.amount_give.value,
                                                         order.expires,
                                                         order.nonce,
                                                         order.user.address,
                                                         order.v if hasattr(order, 'v') else 0,
                                                         order.r if hasattr(order, 'r') else bytes(),
                                                         order.s if hasattr(order, 's') else bytes()))

    def amount_filled(self, order: Order) -> Wad:
        """Returns the amount that has been already filled for an order.

        The result will never be greater than `order.amount_get`. It can be lower though
        if the order maker does not have enough balance on EtherDelta.

        If an order has been cancelled, `amount_filled(order)` will be always equal
        to `order.amount_get`. Cancelled orders basically look like completely filled ones.

        Args:
            order: The order object you want to know the filled amount of.
                Can be either an `OnChainOrder` or an `OffChainOrder`.

        Returns:
            The amount already filled for the order, in terms of `token_get`.
        """
        return Wad(self._contract.call().amountFilled(order.token_get.address,
                                                      order.amount_get.value,
                                                      order.token_give.address,
                                                      order.amount_give.value,
                                                      order.expires,
                                                      order.nonce,
                                                      order.user.address,
                                                      order.v if hasattr(order, 'v') else 0,
                                                      order.r if hasattr(order, 'r') else bytes(),
                                                      order.s if hasattr(order, 's') else bytes()))

    def trade(self, order: Order, amount: Wad) -> Transact:
        """Takes (buys) an order.

        `amount` is in `token_get` terms, it is the amount you want to buy with. It can not be higher
        than `amount_available(order)`.

        The 'amount' of `token_get` tokens will get deducted from your EtherDelta balance if the trade was
        successful. The corresponding amount of `token_have` tokens will be added to your EtherDelta balance.

        Args:
            order: The order you want to take (buy). Can be either an `OnChainOrder` or an `OffChainOrder`.
            amount: Amount of `token_get` tokens that you want to be deducted from your EtherDelta balance
                in order to buy a corresponding amount of `token_have` tokens.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert(isinstance(order, Order))
        assert(isinstance(amount, Wad))

        return Transact(self, self.web3, self.abi, self.address, self._contract, 'trade',
                        [order.token_get.address,
                         order.amount_get.value,
                         order.token_give.address,
                         order.amount_give.value,
                         order.expires,
                         order.nonce,
                         order.user.address,
                         order.v if hasattr(order, 'v') else 0,
                         order.r if hasattr(order, 'r') else bytes(),
                         order.s if hasattr(order, 's') else bytes(),
                         amount.value])

    def can_trade(self, order: Order, amount: Wad) -> bool:
        """Verifies whether a trade can be executed.

        Verifies whether amount `amount` can be traded on order `order` i.e. whether the `trade()`
        method executed with exactly the same parameters should succeed.

        Args:
            order: The order you want to verify the trade for. Can be either an `OnChainOrder` or an `OffChainOrder`.
            amount: Amount expressed in terms of `token_get` that you want to verify the trade for.

        Returns:
            'True' if the given amount can be traded on this order. `False` otherwise.
        """
        assert(isinstance(order, Order))
        assert(isinstance(amount, Wad))

        return self._contract.call().testTrade(order.token_get.address,
                                               order.amount_get.value,
                                               order.token_give.address,
                                               order.amount_give.value,
                                               order.expires,
                                               order.nonce,
                                               order.user.address,
                                               order.v if hasattr(order, 'v') else 0,
                                               order.r if hasattr(order, 'r') else bytes(),
                                               order.s if hasattr(order, 's') else bytes(),
                                               amount.value,
                                               self.web3.eth.defaultAccount)

    def cancel_order(self, order: Order) -> Transact:
        """Cancels an existing order.

        Orders can be cancelled only by their owners.

        Args:
            order: The order you want to cancel. Can be either an `OnChainOrder` or an `OffChainOrder`.

        Returns:
            A :py:class:`keeper.api.Transact` instance, which can be used to trigger the transaction.
        """
        assert(isinstance(order, Order))

        return Transact(self, self.web3, self.abi, self.address, self._contract, 'cancelOrder',
                        [order.token_get.address,
                         order.amount_get.value,
                         order.token_give.address,
                         order.amount_give.value,
                         order.expires,
                         order.nonce,
                         order.v if hasattr(order, 'v') else 0,
                         order.r if hasattr(order, 'r') else bytes(),
                         order.s if hasattr(order, 's') else bytes()])

    @staticmethod
    def random_nonce():
        return random.randint(1, 2**32 - 1)

    @coerce_return_to_text
    def _eth_sign(self, account, data_hash):
        return self.web3._requestManager.request_blocking(
            "eth_sign", [account, encode_hex(data_hash)],
        )

    def __repr__(self):
        return f"EtherDelta('{self.address}')"