Esempio n. 1
0
class DSToken(ERC20Token):
    abi = Contract._load_abi(__name__, 'abi/DSToken.abi')
    bin = Contract._load_bin(__name__, 'abi/DSToken.bin')

    @staticmethod
    def deploy(web3: Web3, symbol: str):
        assert (isinstance(symbol, str))
        return DSToken(web3=web3,
                       address=Contract._deploy(web3, DSToken.abi, DSToken.bin,
                                                [symbol]))

    def set_authority(self, address: Address) -> Optional[Receipt]:
        assert (isinstance(address, Address))
        return self._transact(
            self.web3, f"DSToken('{self.address}').setAuthority('{address}')",
            lambda: self._contract.transact().setAuthority(address.address))

    def mint(self, amount: Wad) -> Optional[Receipt]:
        assert (isinstance(amount, Wad))
        return self._transact(
            self.web3, f"DSToken('{self.address}').mint('{amount}')",
            lambda: self._contract.transact().mint(amount.value))

    def burn(self, amount: Wad) -> Optional[Receipt]:
        assert (isinstance(amount, Wad))
        return self._transact(
            self.web3, f"DSToken('{self.address}').burn('{amount}')",
            lambda: self._contract.transact().burn(amount.value))
Esempio n. 2
0
class DSEthToken(ERC20Token):
    """A client for a the `DSEthToken` contract.

    `DSEthToken`, also known as _ETH Wrapper_ or _W-ETH_, is a smart 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.

    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, args=[]):
        return DSEthToken(web3=web3,
                          address=Contract._deploy(web3, DSEthToken.abi,
                                                   DSEthToken.bin, args))

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

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

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

        Returns:
            A `Receipt` if the Ethereum transaction was successful and the amount has been deposited.
            `None` if the Ethereum transaction failed.
        """
        assert (isinstance(amount, Wad))
        return self._transact(
            self.web3,
            f"DSEthToken('{self.address}').deposit() with value='{amount}'",
            lambda: self._contract.transact({
                'value': amount.value
            }).deposit())

    def withdraw(self, amount: Wad) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful and the amount has been withdrawn.
            `None` if the Ethereum transaction failed.
        """
        assert (isinstance(amount, Wad))
        return self._transact(
            self.web3, f"DSEthToken('{self.address}').withdraw('{amount}')",
            lambda: self._contract.transact().withdraw(amount.value))
Esempio n. 3
0
class 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, []))
Esempio n. 4
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._assert_contract_exists(web3, address)
        self._contract = web3.eth.contract(abi=self.abi)(address=address.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) -> Optional[Receipt]:
        assert(isinstance(address, Address))
        return self._transact(self.web3, f"Top('{self.address}').setAuthority('{address}')",
                              lambda: self._contract.transact().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}')"
Esempio n. 5
0
class 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):
        return DSVault(web3=web3,
                       address=Contract._deploy(web3, DSVault.abi, DSVault.bin,
                                                []))

    def set_authority(self, address: Address) -> Optional[Receipt]:
        assert (isinstance(address, Address))
        return self._transact(
            self.web3, f"DSVault('{self.address}').setAuthority('{address}')",
            lambda: self._contract.transact().setAuthority(address.address))
Esempio n. 6
0
class DSCache(DSValue):
    """A client for the `DSCache` contract, an expiring single-value data feed.

    As `DSCache` inherits from `DSValue`, it replicates most of its features and methods.

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

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

    abi = Contract._load_abi(__name__, 'abi/DSCache.abi')

    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)
Esempio n. 7
0
class 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) -> Optional[Receipt]:
        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 self._transact(self.web3, f"DSGuard('{self.address}').permit('{src}', '{dst}', '{bytes_to_hexstring(sig)}')",
                              lambda: self._contract.transact().permit(src, dst, sig))

    def forbid(self, src: Address, dst: Address, sig: bytes) -> Optional[Receipt]:
        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 self._transact(self.web3, f"DSGuard('{self.address}').forbid('{src}', '{dst}', '{bytes_to_hexstring(sig)}')",
                              lambda: self._contract.transact().forbid(src, dst, sig))
Esempio n. 8
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')

    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 execute(self, contract: bytes, calldata: bytes) -> Optional[Receipt]:
        return self._transact(
            self.web3, f"DSProxy('{self.address}').execute('0x...', '0x....')",
            lambda: self._contract.transact().execute(contract, calldata))
Esempio n. 9
0
class Lpc(Contract):
    """A client for the `SaiLPC` contract, a simple two-token liquidity pool created together for Sai.

    `SaiLPC` relies on an external price feed (the `tip`).

    Makers
    - `pool()` their gems and receive LPS tokens, which are a claim on the pool.
    - `exit()` and trade their LPS tokens for a share of the gems in the pool.

    Takers
    - `take()` and exchange one gem for another, whilst paying a fee (the `gap`). The collected fee goes into the pool.

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

    abi = Contract._load_abi(__name__, 'abi/SaiLPC.abi')
    abiTip = Contract._load_abi(__name__, 'abi/Tip.abi')

    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)
        self._contractTip = web3.eth.contract(abi=self.abiTip)(address=self._contract.call().tip())

    def approve(self, approval_function):
        approval_function(ERC20Token(web3=self.web3, address=self.ref()), self.address, 'Lpc')
        approval_function(ERC20Token(web3=self.web3, address=self.alt()), self.address, 'Lpc')

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

        Returns:
            The address of the ref token.
        """
        return Address(self._contract.call().ref())

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

        Returns:
            The address of the alt token.
        """
        return Address(self._contract.call().alt())

    def pip(self) -> Address:
        """Get the price feed (giving refs per alt).

        You can get the current feed value by calling `tag()`.

        Returns:
            The address of the price feed, which could be a `DSValue`, a `DSCache`, a `Mednianizer` etc.
        """
        return Address(self._contract.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._contract.call().tip())

    def gap(self) -> Wad:
        """Get the spread, charged on `take()`.

        Returns:
            The current value of the spread. `1.0` means no spread. `1.02` means 2% spread.
        """
        return Wad(self._contract.call().gap())

    def lps(self) -> Address:
        """Get the LPS token (liquidity provider shares).

        Returns:
            The address of the LPS token.
        """
        return Address(self._contract.call().lps())

    def jump(self, new_gap: Wad) -> Optional[Receipt]:
        """Update the spread.

        Args:
            new_gap: The new value of the spread (`gap`). `1.0` means no spread. `1.02` means 2% spread.

        Returns:
            A `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(new_gap, Wad)
        return self._transact(self.web3, f"Lpc('{self.address}').jump('{new_gap}')",
                              lambda: self._contract.transact().jump(new_gap.value))

    def tag(self) -> Wad:
        """Get the current price (refs per alt).

        The price is read from the price feed (`tip()`) every time this method gets called.

        Returns:
            The current price (refs per alt).
        """
        return Wad(self._contract.call().tag())

    def pie(self) -> Wad:
        """Get the total pool value (in ref).

        Returns:
            The the total pool value (in ref).
        """
        return Wad(self._contract.call().pie())

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

        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 lps per ref ratio.

        Returns:
            The current lps per ref ratio.
        """
        return Ray(self._contract.call().per())

    def pool(self, token: Address, amount: Wad) -> Optional[Receipt]:
        """Enter the pool, get LPS for ref or alt.

        The `amount` of token `token` will be transferred from your account to the pool.
        In return you will receive some number of LPS tokens, calculated accordingly to `per()`.

        LPS tokens are needed to claim the tokens back (either refs or alts) with `exit()`.

        Args:
            token: The token to enter the pool with (either ref or alt).
            amount: The value (in `token`) to enter the pool with.

        Returns:
            A `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(token, Address)
        assert isinstance(amount, Wad)
        return self._transact(self.web3, f"Lpc('{self.address}').pool('{token}', '{amount}')",
                              lambda: self._contract.transact().pool(token.address, amount.value))

    def exit(self, token: Address, amount: Wad) -> Optional[Receipt]:
        """Exit the pool, exchange LPS for ref or alt.

        The `amount` of token `token` will be credited to your account, as long as there are tokens in the
        pool available. The corresponding number of LPS tokens, calculated accordingly to `per()`, will be
        taken from your account.

        Args:
            token: The token you want to receive from the pool (either ref or alt).
            amount: The value (in `token`) you want to receive from the pool.

        Returns:
            A `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(token, Address)
        assert isinstance(amount, Wad)
        return self._transact(self.web3, f"Lpc('{self.address}').exit('{token}', '{amount}')",
                              lambda: self._contract.transact().exit(token.address, amount.value))

    def take(self, token: Address, amount: Wad) -> Optional[Receipt]:
        """Perform an exchange.

        If `token` is ref, credits `amount` of ref to your account, taking the equivalent amount of alts from you.
        If `token` is alt, credits `amount` of alt to your account, taking the equivalent amount of refs from you.

        The current price (`tag`) is used as the exchange rate.

        Args:
            token: The token you want to get from the pool (either ref or alt).
            amount: The value (in `token`) you want to get from the pool.

        Returns:
            A `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(token, Address)
        assert isinstance(amount, Wad)
        return self._transact(self.web3, f"Lpc('{self.address}').take('{token}', '{amount}')",
                              lambda: self._contract.transact().take(token.address, amount.value))

    def take_calldata(self, token: Address, amount: Wad) -> Calldata:
        return Calldata(self.web3.eth.contract(abi=self.abi).encodeABI('take', [token.address, amount.value]))

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

    def __repr__(self):
        return f"Lpc('{self.address}')"
Esempio n. 10
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._assert_contract_exists(web3, address)
        self._contract = web3.eth.contract(abi=self.abi)(address=address.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) -> Optional[Receipt]:
        assert(isinstance(address, Address))
        return self._transact(self.web3, f"Tap('{self.address}').setAuthority('{address}')",
                              lambda: self._contract.transact().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) -> Optional[Receipt]:
        """Update the current spread (`gap`) for `boom` and `bust`.

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

        Returns:
            A `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(new_gap, Wad)
        return self._transact(self.web3, f"Tap('{self.address}').jump('{new_gap}')",
                              lambda: self._contract.transact().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) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(amount_in_skr, Wad)
        return self._transact(self.web3, f"Tap('{self.address}').boom('{amount_in_skr}')",
                              lambda: self._contract.transact().boom(amount_in_skr.value))

    def boom_calldata(self, amount_in_skr: Wad) -> Calldata:
        return Calldata(self.web3.eth.contract(abi=self.abi).encodeABI('boom', [amount_in_skr]))

    def bust(self, amount_in_skr: Wad) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(amount_in_skr, Wad)
        return self._transact(self.web3, f"Tap('{self.address}').bust('{amount_in_skr}')",
                              lambda: self._contract.transact().bust(amount_in_skr.value))

    def bust_calldata(self, amount_in_skr: Wad) -> Calldata:
        return Calldata(self.web3.eth.contract(abi=self.abi).encodeABI('bust', [amount_in_skr]))

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

    def __repr__(self):
        return f"Tap('{self.address}')"
Esempio n. 11
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._assert_contract_exists(web3, address)
        self._contractTub = web3.eth.contract(abi=self.abiTub)(address=address.address)
        self._contractTip = web3.eth.contract(abi=self.abiTip)(address=self._contractTub.call().tip())
        self._contractJar = web3.eth.contract(abi=self.abiJar)(address=self._contractTub.call().jar())
        self._contractJug = web3.eth.contract(abi=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) -> Optional[Receipt]:
        assert(isinstance(address, Address))
        self._transact(self.web3, f"Tub('{self.address}').setAuthority('{address}')",
                       lambda: self._contractTub.transact().setAuthority(address.address))
        self._transact(self.web3, f"Tip('{self._contractTub.call().tip()}').setAuthority('{address}')",
                       lambda: self._contractTip.transact().setAuthority(address.address))
        self._transact(self.web3, f"SaiJar('{self._contractTub.call().jar()}').setAuthority('{address}')",
                       lambda: self._contractJar.transact().setAuthority(address.address))
        return self._transact(self.web3, f"SaiJug('{self._contractTub.call().jug()}').setAuthority('{address}')",
                       lambda: self._contractJug.transact().setAuthority(address.address))

    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 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) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(new_axe, Ray)
        return self._transact(self.web3, f"Tub('{self.address}').chop('{new_axe}')",
                              lambda: self._contractTub.transact().chop(new_axe.value))

    def cork(self, new_hat: Wad) -> Optional[Receipt]:
        """Update the debt ceiling.

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

        Returns:
            A `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(new_hat, Wad)
        return self._transact(self.web3, f"Tub('{self.address}').cork('{new_hat}')",
                              lambda: self._contractTub.transact().cork(new_hat.value))

    def cuff(self, new_mat: Ray) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(new_mat, Ray)
        return self._transact(self.web3, f"Tub('{self.address}').cuff('{new_mat}')",
                              lambda: self._contractTub.transact().cuff(new_mat.value))

    def crop(self, new_tax: Ray) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(new_tax, Ray)
        return self._transact(self.web3, f"Tub('{self.address}').crop('{new_tax}')",
                              lambda: self._contractTub.transact().crop(new_tax.value))

    def coax(self, new_way: Ray) -> Optional[Receipt]:
        """Update the holder fee.

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

        Returns:
            A `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(new_way, Ray)
        return self._transact(self.web3, f"Tip('{self._contractTub.call().tip()}').coax('{new_way}')",
                              lambda: self._contractTip.transact().coax(new_way.value))

    def drip(self) -> Optional[Receipt]:
        """Recalculate the internal debt price (`chi`).

        Returns:
            A `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        return self._transact(self.web3, f"Tub('{self.address}').drip()",
                              lambda: self._contractTub.transact().drip())

    def prod(self) -> Optional[Receipt]:
        """Recalculate the accrued holder fee (`par`).

        Returns:
            A `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        return self._transact(self.web3, f"Tip('{self._contractTub.call().tip()}').prod()",
                              lambda: self._contractTip.transact().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) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(new_gap, Wad)
        return self._transact(self.web3, f"Jar('{self._contractTub.call().jar()}').jump('{new_gap}')",
                              lambda: self._contractJar.transact().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) -> Optional[Receipt]:
        """Buy SKR for GEMs.

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

        Returns:
            A `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(amount_in_gem, Wad)
        return self._transact(self.web3, f"Tub('{self.address}').join('{amount_in_gem}')",
                              lambda: self._contractTub.transact().join(amount_in_gem.value))

    def join_calldata(self, amount_in_gem: Wad) -> Calldata:
        return Calldata(self.web3.eth.contract(abi=self.abiTub).encodeABI('join', [amount_in_gem]))

    def exit(self, amount_in_skr: Wad) -> Optional[Receipt]:
        """Sell SKR for GEMs.

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

        Returns:
            A `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(amount_in_skr, Wad)
        return self._transact(self.web3, f"Tub('{self.address}').exit('{amount_in_skr}')",
                              lambda: self._contractTub.transact().exit(amount_in_skr.value))

    def exit_calldata(self, amount_in_skr: Wad) -> Calldata:
        return Calldata(self.web3.eth.contract(abi=self.abiTub).encodeABI('exit', [amount_in_skr]))

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

        Returns:
            A `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        return self._transact(self.web3, f"Tub('{self.address}').open()",
                              lambda: self._contractTub.transact().open())

    def shut(self, cup_id: int) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(cup_id, int)
        return self._transact(self.web3, f"Tub('{self.address}').shut('{cup_id}')",
                              lambda: self._contractTub.transact().shut(int_to_bytes32(cup_id)))

    def lock(self, cup_id: int, amount_in_skr: Wad) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(cup_id, int)
        assert isinstance(amount_in_skr, Wad)
        return self._transact(self.web3, f"Tub('{self.address}').lock('{cup_id}', '{amount_in_skr}')",
                              lambda: self._contractTub.transact().lock(int_to_bytes32(cup_id), amount_in_skr.value))

    def free(self, cup_id: int, amount_in_skr: Wad) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(cup_id, int)
        assert isinstance(amount_in_skr, Wad)
        return self._transact(self.web3, f"Tub('{self.address}').free('{cup_id}', '{amount_in_skr}')",
                              lambda: self._contractTub.transact().free(int_to_bytes32(cup_id), amount_in_skr.value))

    def draw(self, cup_id: int, amount_in_sai: Wad) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(cup_id, int)
        assert isinstance(amount_in_sai, Wad)
        return self._transact(self.web3, f"Tub('{self.address}').draw('{cup_id}', '{amount_in_sai}')",
                              lambda: self._contractTub.transact().draw(int_to_bytes32(cup_id), amount_in_sai.value))

    def wipe(self, cup_id: int, amount_in_sai: Wad) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(cup_id, int)
        assert isinstance(amount_in_sai, Wad)
        return self._transact(self.web3, f"Tub('{self.address}').wipe('{cup_id}', '{amount_in_sai}')",
                              lambda: self._contractTub.transact().wipe(int_to_bytes32(cup_id), amount_in_sai.value))

    def give(self, cup_id: int, new_lad: Address) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(cup_id, int)
        assert isinstance(new_lad, Address)
        return self._transact(self.web3, f"Tub('{self.address}').give('{cup_id}', '{new_lad}')",
                              lambda: self._contractTub.transact().give(int_to_bytes32(cup_id), new_lad.address))

    def bite(self, cup_id: int) -> Optional[Receipt]:
        """Initiate liquidation of an undercollateralized cup.

        Args:
            cup_id: Id of the cup to liquidate.

        Returns:
            A `Receipt` if the Ethereum transaction was successful.
            `None` if the Ethereum transaction failed.
        """
        assert isinstance(cup_id, int)
        return self._transact(self.web3, f"Tub('{self.address}').bite('{cup_id}')",
                              lambda: self._contractTub.transact().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}')"
Esempio n. 12
0
class ERC20Token(Contract):
    """A client for a standard ERC20 token contract.

    Attributes:
        web3: An instance of `Web` from `web3.py`.
        address: Ethereum address of the ERC20 token.
    """

    abi = Contract._load_abi(__name__, 'abi/ERC20Token.abi')
    registry = {}

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

    def name(self):
        return ERC20Token.registry.get(self.address, '???')

    def total_supply(self) -> Wad:
        """Returns the total supply of the token.
        
        Returns:
            The total supply of the token.
        """
        return Wad(self._contract.call().totalSupply())

    def balance_of(self, address: Address) -> Wad:
        """Returns the token balance of a given address.

        Args:
            address: The address to check the balance of.

        Returns:
            The token balance of the address specified.
        """
        return Wad(self._contract.call().balanceOf(address.address))

    def allowance_of(self, address: Address, payee: Address) -> Wad:
        """Returns the current allowance of a specified `payee` (delegate account).

        Allowance is an ERC20 concept allowing the `payee` (delegate account) to spend a fixed amount of tokens
        on behalf of the token owner (`address`).

        Args:
            address: The address to check the allowance for (it's the address the tokens can be spent from).
            payee: The address of the delegate account (it's the address that can spend the tokens).

        Returns:
            The allowance of the `payee` specified in regards to the `address`.
        """
        return Wad(self._contract.call().allowance(address.address,
                                                   payee.address))

    def transfer(self, address: Address, value: Wad) -> Optional[Receipt]:
        """Transfers tokens to a specified address.

        Args:
            address: destination address to transfer the tokens to.
            value: the value of tokens to transfer.

        Returns:
            A `Receipt` if the Ethereum transaction (and thus the token transfer) was successful.
            `None` if the Ethereum transaction failed.
        """
        return self._transact(
            self.web3,
            f"ERC20Token('{self.address}').transfer('{address}', '{value}')",
            lambda: self._contract.transact().transfer(address.address, value.
                                                       value))

    def transfer_calldata(self, address: Address,
                          value: Wad) -> Optional[Receipt]:
        return Calldata(
            self.web3.eth.contract(abi=self.abi).encodeABI(
                'transfer', [address.address, value.value]))

    def approve(
        self, payee: Address,
        limit: Wad = Wad(2**256 - 1)) -> Optional[Receipt]:
        """Modifies the current allowance of a specified `payee` (delegate account).

        Allowance is an ERC20 concept allowing the `payee` (delegate account) to spend a fixed amount of tokens
        (`limit`) on behalf of the token owner.

        If `limit` is omitted, a maximum possible value is granted.

        Args:
            payee: The address of the delegate account (it's the address that can spend the tokens).
            limit: The value of the allowance i.e. the value of tokens that the `payee` (delegate account)
                can spend on behalf of their owner.

        Returns:
            A `Receipt` if the Ethereum transaction (and thus the approval) was successful.
            `None` if the Ethereum transaction failed.
        """
        return self._transact(
            self.web3,
            f"ERC20Token('{self.address}').approve('{payee}', '{limit}')",
            lambda: self._contract.transact().approve(payee.address, limit.
                                                      value))

    def approve_calldata(
        self, payee: Address, limit: Wad = Wad(2**256 - 1)) -> Calldata:
        return Calldata(
            self.web3.eth.contract(abi=self.abi).encodeABI(
                'approve', [payee.address, limit.value]))

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

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

    @staticmethod
    def register_token(address: Address, name):
        ERC20Token.registry[address] = name

    @staticmethod
    def token_address_by_name(token_name):
        for address, name in ERC20Token.registry.items():
            if name == token_name:
                return address
        raise Exception(f"Token {token_name} not found")

    @staticmethod
    def token_name_by_address(token_address: Address):
        return ERC20Token.registry[token_address]
Esempio n. 13
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')

    ETH_TOKEN = Address('0x0000000000000000000000000000000000000000')

    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._assert_contract_exists(web3, address)
        self._contract = web3.eth.contract(abi=self.abi)(
            address=address.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 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) -> Optional[Receipt]:
        """Deposits `amount` of raw ETH to EtherDelta.

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

        Returns:
            A `Receipt` if the Ethereum transaction was successful and the amount has been deposited.
            `None` if the Ethereum transaction failed.
        """
        assert (isinstance(amount, Wad))
        return self._transact(
            self.web3,
            f"EtherDelta('{self.address}').deposit() with value='{amount}'",
            lambda: self._contract.transact({
                'value': amount.value
            }).deposit())

    def withdraw(self, amount: Wad) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful and the amount has been withdrawn.
            `None` if the Ethereum transaction failed.
        """
        assert (isinstance(amount, Wad))
        return self._transact(
            self.web3, f"EtherDelta('{self.address}').withdraw('{amount}')",
            lambda: self._contract.transact().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) -> Optional[Receipt]:
        """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 on EtherDelta.

        Returns:
            A `Receipt` if the Ethereum transaction was successful and the tokens have been deposited.
            `None` if the Ethereum transaction failed.
        """
        assert (isinstance(token, Address))
        assert (isinstance(amount, Wad))
        return self._transact(
            self.web3,
            f"EtherDelta('{self.address}').depositToken('{token}', '{amount}')",
            lambda: self._contract.transact().depositToken(
                token.address, amount.value))

    def withdraw_token(self, token: Address, amount: Wad) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful and the tokens have been withdrawn.
            `None` if the Ethereum transaction failed.
        """
        assert (isinstance(token, Address))
        assert (isinstance(amount, Wad))
        return self._transact(
            self.web3,
            f"EtherDelta('{self.address}').withdrawToken('{token}', '{amount}')",
            lambda: self._contract.transact().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[OnChainOrder]:
        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) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful and the order has been placed.
            `None` if the Ethereum transaction failed.
        """
        nonce = self.random_nonce()
        result = self._transact(
            self.web3,
            f"EtherDelta('{self.address}').order('{token_get}', '{amount_get}',"
            f" '{token_give}', '{amount_give}', '{expires}', '{nonce}')",
            lambda: self._contract.transact(
            ).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 result is not None and self._onchain_orders is not None:
            onchain_order = OnChainOrder(token_get, amount_get, token_give,
                                         amount_give, expires, nonce,
                                         Address(self.web3.eth.defaultAccount))

            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)

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

        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)

        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

    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) -> Optional[Receipt]:
        """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 `available_volume(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 `Receipt` if the Ethereum transaction was successful and so was the trade.
            `None` if the Ethereum transaction failed.
        """
        assert (isinstance(order, Order))
        assert (isinstance(amount, Wad))

        return self._transact(
            self.web3,
            f"EtherDelta('{self.address}').trade('{order.token_get}',"
            f" '{order.amount_get}', '{order.token_give}', '{order.amount_give}',"
            f" '{order.expires}', '{order.nonce}', '{order.user}', '0x...', '0x...',"
            f" '0x...', '{amount}')", lambda: self._contract.transact().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) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful and the order has been cancelled.
            `None` if the Ethereum transaction failed.
        """
        assert (isinstance(order, Order))

        return self._transact(
            self.web3,
            f"EtherDelta('{self.address}').cancelOrder('{order.token_get}',"
            f" '{order.amount_get}', '{order.token_give}', '{order.amount_give}',"
            f" '{order.expires}', '{order.nonce}', '0x...', '0x...', '0x...')",
            lambda: self._contract.transact().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)],
        )
Esempio n. 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, *args):
        return DSValue(web3=web3,
                       address=Contract._deploy(web3, DSValue.abi, DSValue.bin,
                                                args))

    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) -> Optional[Receipt]:
        """Populates this instance with a new value.

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

        Returns:
            A `Receipt` if the Ethereum transaction was successful and the value has been set.
            `None` if the Ethereum transaction failed.
        """
        assert (isinstance(new_value, bytes))
        assert (len(new_value) == 32)
        return self._transact(
            self.web3, f"DSValue('{self.address}').poke('{new_value}')",
            lambda: self._contract.transact().poke(new_value))

    def poke_with_int(self, new_value: int) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful and the value has been set.
            `None` if the Ethereum transaction failed.
        """
        assert (isinstance(new_value, int))
        assert (new_value >= 0)
        return self.poke(new_value.to_bytes(32, byteorder='big'))

    def void(self) -> Optional[Receipt]:
        """Removes the current value from this instance.

        Returns:
            A `Receipt` if the Ethereum transaction was successful and the value has been removed.
            `None` if the Ethereum transaction failed.
        """
        return self._transact(self.web3, f"DSValue('{self.address}').void()",
                              lambda: self._contract.transact().void())
Esempio n. 15
0
class TxManager(Contract):
    """A client for the `TxManager` contract.

    `TxManager` allows to invoke multiple smart 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`.

    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._assert_contract_exists(web3, address)
        self._contract = web3.eth.contract(abi=self.abi)(
            address=address.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]) -> Optional[Receipt]:
        """Executes multiple smart 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 (smart contract methods) to be executed.
        """
        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 self._transact(
            self.web3,
            f"TxManager('{self.address}').execute('0x...', '0x....')",
            lambda: self._contract.transact().execute(token_addresses(),
                                                      script()))
Esempio n. 16
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 `SimpleMarket` contract 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')

    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)
        self._none_offers = set()

    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) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful and the offer has been created.
            `None` if the Ethereum transaction failed.
        """
        return self._transact(
            self.web3, f"SimpleMarket('{self.address}')"
            f".make('{have_token}', '{want_token}', '{have_amount}', '{want_amount}')",
            lambda: self._contract.transact(
            ).make(have_token.address, want_token.address, have_amount.value,
                   want_amount.value))

    def take(self, offer_id: int, quantity: Wad) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful and the offer has been taken (bought).
            `None` if the Ethereum transaction failed.
        """
        return self._transact(
            self.web3,
            f"SimpleMarket('{self.address}').take('{offer_id}', '{quantity}')",
            lambda: self._contract.transact().take(int_to_bytes32(offer_id),
                                                   quantity.value))

    def take_calldata(self, offer_id: int, quantity: Wad) -> Calldata:
        return Calldata(
            self.web3.eth.contract(abi=self.abi).encodeABI(
                'take', [int_to_bytes32(offer_id), quantity.value]))

    def kill(self, offer_id: int) -> Optional[Receipt]:
        """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 `Receipt` if the Ethereum transaction was successful and the offer has been cancelled.
            `None` if the Ethereum transaction failed.
        """
        return self._transact(
            self.web3, f"SimpleMarket('{self.address}').kill('{offer_id}')",
            lambda: self._contract.transact().kill(int_to_bytes32(offer_id)))

    async def kill_async(self, offer_id: int) -> Optional[Receipt]:
        return await self._async_transact(
            self.web3, f"SimpleMarket('{self.address}').kill('{offer_id}')",
            lambda: self._contract.transact().kill(int_to_bytes32(offer_id)))