Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
724 lines (552 sloc)
30.6 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This file is part of Maker Keeper Framework. | |
# | |
# Copyright (C) 2017-2018 reverendus | |
# | |
# This program is free software: you can redistribute it and/or modify | |
# it under the terms of the GNU Affero General Public License as published by | |
# the Free Software Foundation, either version 3 of the License, or | |
# (at your option) any later version. | |
# | |
# This program is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
# GNU Affero General Public License for more details. | |
# | |
# You should have received a copy of the GNU Affero General Public License | |
# along with this program. If not, see <http://www.gnu.org/licenses/>. | |
from pprint import pformat | |
from typing import Optional, List, Iterable, Iterator | |
from hexbytes import HexBytes | |
from web3 import Web3 | |
from web3._utils.events import get_event_data | |
from eth_abi.codec import ABICodec | |
from eth_abi.registry import registry as default_registry | |
from pymaker import Contract, Address, Transact, Receipt | |
from pymaker.numeric import Wad | |
from pymaker.token import ERC20Token | |
from pymaker.util import int_to_bytes32, bytes_to_int | |
from pymaker.model import Token | |
class Order: | |
"""Represents a single order on `OasisDEX`. | |
Instances of this class shouldn't be created directly. Instead of that, new orders can be queried | |
using methods of :py:class:`pymaker.oasis.SimpleMarket` or :py:class:`pymaker.oasis.MatchingMarket`. | |
Attributes: | |
order_id: Id of the order. | |
maker: Ethereum address of the owner of this order. | |
pay_token: The address of the token which is put on sale. | |
pay_amount: The amount of the `pay_token` token which is put on sale. | |
buy_token: The address of the token the order creator wants to be paid with. | |
buy_amount: The price the order creator wants to be paid, denominated in the `buy_token` token. | |
timestamp: Date and time when this order has been created, as a unix timestamp. | |
""" | |
def __init__(self, market, order_id: int, maker: Address, pay_token: Address, pay_amount: Wad, buy_token: Address, | |
buy_amount: Wad, timestamp: int): | |
assert(isinstance(order_id, int)) | |
assert(isinstance(maker, Address)) | |
assert(isinstance(pay_token, Address)) | |
assert(isinstance(pay_amount, Wad)) | |
assert(isinstance(buy_token, Address)) | |
assert(isinstance(buy_amount, Wad)) | |
assert(isinstance(timestamp, int)) | |
self._market = market | |
self.order_id = order_id | |
self.maker = maker | |
self.pay_token = pay_token | |
self.pay_amount = pay_amount | |
self.buy_token = buy_token | |
self.buy_amount = buy_amount | |
self.timestamp = timestamp | |
@property | |
def sell_to_buy_price(self) -> Wad: | |
return self.pay_amount / self.buy_amount | |
@property | |
def buy_to_sell_price(self) -> Wad: | |
return self.buy_amount / self.pay_amount | |
@property | |
def remaining_buy_amount(self) -> Wad: | |
return self.buy_amount | |
@property | |
def remaining_sell_amount(self) -> Wad: | |
return self.pay_amount | |
def __eq__(self, other): | |
assert(isinstance(other, Order)) | |
return self._market.address == other._market.address and self.order_id == other.order_id | |
def __hash__(self): | |
return self.order_id | |
def __repr__(self): | |
return pformat(vars(self)) | |
class LogMake: | |
def __init__(self, log): | |
self.order_id = bytes_to_int(log['args']['id']) | |
self.maker = Address(log['args']['maker']) | |
self.pay_token = Address(log['args']['pay_gem']) | |
self.pay_amount = Wad(log['args']['pay_amt']) | |
self.buy_token = Address(log['args']['buy_gem']) | |
self.buy_amount = Wad(log['args']['buy_amt']) | |
self.timestamp = log['args']['timestamp'] | |
self.raw = log | |
@classmethod | |
def from_receipt(cls, receipt: Receipt): | |
assert(isinstance(receipt, Receipt)) | |
if receipt.logs is not None: | |
for log in receipt.logs: | |
if len(log['topics']) > 0 and log['topics'][0] == HexBytes('0x773ff502687307abfa024ac9f62f9752a0d210dac2ffd9a29e38e12e2ea82c82'): | |
log_make_abi = [abi for abi in SimpleMarket.abi if abi.get('name') == 'LogMake'][0] | |
codec = ABICodec(default_registry) | |
event_data = get_event_data(codec, log_make_abi, log) | |
yield LogMake(event_data) | |
def __repr__(self): | |
return pformat(vars(self)) | |
class LogBump: | |
def __init__(self, log): | |
self.order_id = bytes_to_int(log['args']['id']) | |
self.maker = Address(log['args']['maker']) | |
self.pay_token = Address(log['args']['pay_gem']) | |
self.pay_amount = Wad(log['args']['pay_amt']) | |
self.buy_token = Address(log['args']['buy_gem']) | |
self.buy_amount = Wad(log['args']['buy_amt']) | |
self.timestamp = log['args']['timestamp'] | |
self.raw = log | |
def __repr__(self): | |
return pformat(vars(self)) | |
class LogTake: | |
def __init__(self, log): | |
self.order_id = bytes_to_int(log['args']['id']) | |
self.maker = Address(log['args']['maker']) | |
self.taker = Address(log['args']['taker']) | |
self.pay_token = Address(log['args']['pay_gem']) | |
self.take_amount = Wad(log['args']['take_amt']) | |
self.buy_token = Address(log['args']['buy_gem']) | |
self.give_amount = Wad(log['args']['give_amt']) | |
self.timestamp = log['args']['timestamp'] | |
self.raw = log | |
@classmethod | |
def from_event(cls, event: dict): | |
assert(isinstance(event, dict)) | |
topics = event.get('topics') | |
if topics and topics[0] == HexBytes('0x3383e3357c77fd2e3a4b30deea81179bc70a795d053d14d5b7f2f01d0fd4596f'): | |
log_take_abi = [abi for abi in SimpleMarket.abi if abi.get('name') == 'LogTake'][0] | |
codec = ABICodec(default_registry) | |
event_data = get_event_data(codec, log_take_abi, event) | |
return LogTake(event_data) | |
def __eq__(self, other): | |
assert(isinstance(other, LogTake)) | |
return self.__dict__ == other.__dict__ | |
def __repr__(self): | |
return pformat(vars(self)) | |
class LogKill: | |
def __init__(self, log): | |
self.order_id = bytes_to_int(log['args']['id']) | |
self.maker = Address(log['args']['maker']) | |
self.pay_token = Address(log['args']['pay_gem']) | |
self.pay_amount = Wad(log['args']['pay_amt']) | |
self.buy_token = Address(log['args']['buy_gem']) | |
self.buy_amount = Wad(log['args']['buy_amt']) | |
self.timestamp = log['args']['timestamp'] | |
self.raw = log | |
def __repr__(self): | |
return pformat(vars(self)) | |
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): | |
assert(isinstance(web3, Web3)) | |
assert(isinstance(address, Address)) | |
self.web3 = web3 | |
self.address = address | |
self._contract = self._get_contract(web3, self.abi, address) | |
@staticmethod | |
def deploy(web3: Web3): | |
"""Deploy a new instance of the `SimpleMarket` contract. | |
Args: | |
web3: An instance of `Web3` 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): | |
"""Approve the OasisDEX contract to fully access balances of specified tokens. | |
For available approval functions (i.e. approval modes) see `directly` and `via_tx_manager` | |
in `pymaker.approval`. | |
Args: | |
tokens: List of :py:class:`pymaker.token.ERC20Token` class instances. | |
approval_function: Approval function (i.e. approval mode). | |
""" | |
assert(isinstance(tokens, list)) | |
assert(callable(approval_function)) | |
for token in tokens: | |
approval_function(token, self.address, 'OasisDEX') | |
def past_make(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogMake]: | |
"""Synchronously retrieve past LogMake events. | |
`LogMake` events are emitted by the Oasis contract every time someone places an order. | |
Args: | |
number_of_past_blocks: Number of past Ethereum blocks to retrieve the events from. | |
event_filter: Filter which will be applied to returned events. | |
Returns: | |
List of past `LogMake` events represented as :py:class:`pymaker.oasis.LogMake` class. | |
""" | |
assert(isinstance(number_of_past_blocks, int)) | |
assert(isinstance(event_filter, dict) or (event_filter is None)) | |
return self._past_events(self._contract, 'LogMake', LogMake, number_of_past_blocks, event_filter) | |
def past_bump(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogBump]: | |
"""Synchronously retrieve past LogBump events. | |
`LogBump` events are emitted by the Oasis contract every time someone calls the `bump()` function. | |
Args: | |
number_of_past_blocks: Number of past Ethereum blocks to retrieve the events from. | |
event_filter: Filter which will be applied to returned events. | |
Returns: | |
List of past `LogBump` events represented as :py:class:`pymaker.oasis.LogBump` class. | |
""" | |
assert(isinstance(number_of_past_blocks, int)) | |
assert(isinstance(event_filter, dict) or (event_filter is None)) | |
return self._past_events(self._contract, 'LogBump', LogBump, number_of_past_blocks, event_filter) | |
def past_take(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogTake]: | |
"""Synchronously retrieve past LogTake events. | |
`LogTake` events are emitted by the Oasis contract every time someone takes an order. | |
Args: | |
number_of_past_blocks: Number of past Ethereum blocks to retrieve the events from. | |
event_filter: Filter which will be applied to returned events. | |
Returns: | |
List of past `LogTake` events represented as :py:class:`pymaker.oasis.LogTake` class. | |
""" | |
assert(isinstance(number_of_past_blocks, int)) | |
assert(isinstance(event_filter, dict) or (event_filter is None)) | |
return self._past_events(self._contract, 'LogTake', LogTake, number_of_past_blocks, event_filter) | |
def past_kill(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogKill]: | |
"""Synchronously retrieve past LogKill events. | |
`LogKill` events are emitted by the Oasis contract every time someone cancels an order. | |
Args: | |
number_of_past_blocks: Number of past Ethereum blocks to retrieve the events from. | |
event_filter: Filter which will be applied to returned events. | |
Returns: | |
List of past `LogKill` events represented as :py:class:`pymaker.oasis.LogKill` class. | |
""" | |
assert(isinstance(number_of_past_blocks, int)) | |
assert(isinstance(event_filter, dict) or (event_filter is None)) | |
return self._past_events(self._contract, 'LogKill', LogKill, number_of_past_blocks, event_filter) | |
def get_last_order_id(self) -> int: | |
"""Get the id of the last order created on the market. | |
Returns: | |
The id of the last order. Returns `0` if no orders have been created at all. | |
""" | |
return self._contract.functions.last_offer_id().call() | |
def get_order(self, order_id: int, block_ident: Optional[str] = None) -> Optional[Order]: | |
"""Get order details. | |
Args: | |
order_id: The id of the order to get the details of. | |
`block_ident`: Block identifier can either be a str, int or None and will fallback to 'latest' block. | |
Used for historical retreival of orders. | |
Returns: | |
An instance of `Order` if the order is still active, or `None` if the order has been | |
either already completely taken or cancelled. | |
""" | |
assert(isinstance(order_id, int)) | |
assert(isinstance(block_ident, str) or isinstance(block_ident, int) or (block_ident is None)) | |
block_ident = 'latest' if block_ident is None else block_ident | |
array = self._contract.functions.offers(order_id).call(block_identifier=block_ident) | |
if array[5] == 0: | |
return None | |
else: | |
return Order(market=self, order_id=order_id, maker=Address(array[4]), pay_token=Address(array[1]), | |
pay_amount=Wad(array[0]), buy_token=Address(array[3]), buy_amount=Wad(array[2]), | |
timestamp=array[5]) | |
def get_orders(self, pay_token: Address = None, buy_token: Address = None) -> List[Order]: | |
"""Get all active orders. | |
If both `pay_token` and `buy_token` are specified, orders will be filtered by these. | |
Either none or both of these parameters have to be specified. | |
Args: | |
`pay_token`: Address of the `pay_token` to filter the orders by. | |
`buy_token`: Address of the `buy_token` to filter the orders by. | |
Returns: | |
A list of `Order` objects representing all active orders on Oasis. | |
""" | |
assert((isinstance(pay_token, Address) and isinstance(buy_token, Address)) | |
or (pay_token is None and buy_token is None)) | |
orders = [self.get_order(order_id + 1) for order_id in range(self.get_last_order_id())] | |
orders = [order for order in orders if order is not None] | |
if pay_token is not None and buy_token is not None: | |
orders = list(filter(lambda order: order.pay_token == pay_token and order.buy_token == buy_token, orders)) | |
return orders | |
def get_orders_by_maker(self, maker: Address) -> List[Order]: | |
"""Get all active orders created by `maker`. | |
Args: | |
maker: Address of the `maker` to filter the orders by. | |
Returns: | |
A list of `Order` objects representing all active orders belonging to this `maker`. | |
""" | |
assert(isinstance(maker, Address)) | |
result = [] | |
for order_id in range(self.get_last_order_id()): | |
# Query the order. | |
order = self.get_order(order_id + 1) | |
if order is None: | |
continue | |
# We are only interested in orders owned by `maker`. In case the order is not owned by `maker`, | |
# we add it to `_alien_orders[maker]` so the next time `get_orders_by_maker()` is called | |
# with the same parameter we will be able to rule out these orders straight away. | |
if order.maker != maker: | |
continue | |
result.append(order) | |
return result | |
def make(self, pay_token: Address, pay_amount: Wad, buy_token: Address, buy_amount: Wad) -> Transact: | |
"""Create a new order. | |
The `pay_amount` of `pay_token` token will be taken from you on order creation and deposited | |
in the market contract. Allowance needs to be set first - refer to the `approve()` method. | |
When complete, `receipt.result` will contain order_id of the new order. | |
Args: | |
pay_token: Address of the ERC20 token you want to put on sale. | |
pay_amount: Amount of the `pay_token` token you want to put on sale. | |
buy_token: Address of the ERC20 token you want to be paid with. | |
buy_amount: Amount of the `buy_token` you want to receive. | |
Returns: | |
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. | |
""" | |
assert(isinstance(pay_token, Address)) | |
assert(isinstance(pay_amount, Wad)) | |
assert(isinstance(buy_token, Address)) | |
assert(isinstance(buy_amount, Wad)) | |
assert(pay_amount > Wad(0)) | |
assert(buy_amount > Wad(0)) | |
return Transact(self, self.web3, self.abi, self.address, self._contract, | |
'make', [pay_token.address, buy_token.address, pay_amount.value, buy_amount.value], None, | |
self._make_order_id_result_function) | |
def bump(self, order_id: int) -> Transact: | |
"""Bumps an order. | |
Bumping an order generates a `LogBump` event, which can make the order reappear | |
in some front-ends relying on the events. | |
Args: | |
order_id: Id of the order you want to bump. | |
Returns: | |
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. | |
""" | |
assert(isinstance(order_id, int)) | |
return Transact(self, self.web3, self.abi, self.address, self._contract, 'bump', | |
[int_to_bytes32(order_id)]) | |
def take(self, order_id: int, quantity: Wad) -> Transact: | |
"""Takes (buys) an order. | |
If `quantity` is equal to `pay_amount`, the whole order will be taken (bought) which will make it | |
disappear from the order book. If you want to buy a fraction of the order, set `quantity` to a number | |
lower than `pay_amount`. | |
Args: | |
order_id: Id of the order you want to take (buy). | |
quantity: Quantity of `pay_token` that you want to buy. | |
Returns: | |
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. | |
""" | |
assert(isinstance(order_id, int)) | |
assert(isinstance(quantity, Wad)) | |
return Transact(self, self.web3, self.abi, self.address, self._contract, 'take', | |
[int_to_bytes32(order_id), quantity.value]) | |
def kill(self, order_id: int) -> Transact: | |
"""Cancels an existing order. | |
Orders can be cancelled only by their owners. | |
Args: | |
order_id: Id of the order you want to cancel. | |
Returns: | |
A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. | |
""" | |
assert(isinstance(order_id, int)) | |
return Transact(self, self.web3, self.abi, self.address, self._contract, 'kill(bytes32)', [int_to_bytes32(order_id)]) | |
@staticmethod | |
def _make_order_id_result_function(receipt): | |
return next(map(lambda log_make: log_make.order_id, LogMake.from_receipt(receipt)), None) | |
def __repr__(self): | |
return f"SimpleMarket('{self.address}')" | |
class MatchingMarket(SimpleMarket): | |
"""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. | |
support_address: Ethereum address of the `MakerOtcSupportMethods` contract (optional). | |
""" | |
abi = Contract._load_abi(__name__, 'abi/MatchingMarket.abi') | |
bin = Contract._load_bin(__name__, 'abi/MatchingMarket.bin') | |
abi_support = Contract._load_abi(__name__, 'abi/MakerOtcSupportMethods.abi') | |
def __init__(self, web3: Web3, address: Address, support_address: Optional[Address] = None): | |
assert(isinstance(support_address, Address) or (support_address is None)) | |
super(MatchingMarket, self).__init__(web3=web3, address=address) | |
self.support_address = support_address | |
self._support_contract = self._get_contract(web3, self.abi_support, self.support_address) \ | |
if self.support_address else None | |
@staticmethod | |
def deploy(web3: Web3, dust_token: Address, dust_limit: Wad, price_oracle: Address, | |
support_address: Optional[Address] = None): | |
"""Deploy a new instance of the `MatchingMarket` contract. | |
Args: | |
web3: An instance of `Web` from `web3.py`. | |
dust_token: Address of token serving as unit of measurement for dust_limit | |
dust_limit: The limit itself | |
price_oracle: Exposes getPriceFor method for price conversion | |
support_address: Ethereum address of the `MakerOtcSupportMethods` contract (optional). | |
Returns: | |
A `MatchingMarket` class instance. | |
""" | |
assert isinstance(dust_token, Address) | |
assert isinstance(dust_limit, Wad) | |
assert isinstance(price_oracle, Address) | |
return MatchingMarket(web3=web3, address=Contract._deploy(web3, MatchingMarket.abi, MatchingMarket.bin, | |
[dust_token.address, | |
dust_limit.value, | |
price_oracle.address]), | |
support_address=support_address) | |
def add_token_pair_whitelist(self, base_token: Address, quote_token: Address) -> Transact: | |
"""Adds a token pair to the whitelist. | |
All newly created orders are checked against the whitelist. | |
Args: | |
base_token: Address of the ERC20 token. | |
quote_token: Address of the ERC20 token. | |
Returns: | |
A :py:class:`pymaker.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 get_orders(self, p_token: Token = None, b_token: Token = None, block_ident: Optional[str] = None) -> List[Order]: | |
"""Get all active orders. | |
If both `p_token` and `b_token` are specified, orders will be filtered by these. | |
In case of the _MatchingMarket_ implementation, order enumeration will be much efficient | |
if these two parameters are supplied, as then orders can be fetched using `getBestOffer` | |
and a series of `getWorseOffer` calls. This approach will result in much lower number of calls | |
comparing to the naive 0..get_last_order_id approach, especially if the number of inactive orders | |
is very high. | |
Either none or both of these parameters have to be specified. | |
Args: | |
`p_token`: Token object (see `model.py`) of the `pay_token` to filter the orders by. | |
`b_token`: Token object (see `model.py`) of the `buy_token` to filter the orders by. | |
`block_ident`: Block identifier can either be a str, int or None and will fallback to 'latest' block. | |
Used for historical retreival of orders. | |
Returns: | |
A list of `Order` objects representing all active orders on Oasis. | |
""" | |
assert((isinstance(p_token, Token) and isinstance(b_token, Token)) | |
or ((p_token is None) and (b_token is None))) | |
assert(isinstance(block_ident, int) or isinstance(block_ident, str) or (block_ident is None)) | |
block_ident = 'latest' if block_ident is None else block_ident | |
if (p_token is None) or (b_token is None): | |
pay_token = None | |
buy_token = None | |
else: | |
pay_token = p_token.address | |
buy_token = b_token.address | |
if pay_token is not None and buy_token is not None: | |
orders = [] | |
if self._support_contract: | |
result = self._support_contract.functions.getOffers(self.address.address, pay_token.address, buy_token.address).call(block_identifier=block_ident) | |
while True: | |
count = 0 | |
for i in range(0, 100): | |
if result[3][i] != '0x0000000000000000000000000000000000000000': | |
count += 1 | |
orders.append(Order(market=self, | |
order_id=result[0][i], | |
maker=Address(result[3][i]), | |
pay_token=pay_token, | |
pay_amount=p_token.normalize_amount(Wad(result[1][i])), | |
buy_token=buy_token, | |
buy_amount=b_token.normalize_amount(Wad(result[2][i])), | |
timestamp=result[4][i])) | |
if count == 100: | |
next_order_id = self._contract.functions.getWorseOffer(orders[-1].order_id).call(block_identifier=block_ident) | |
result = self._support_contract.functions.getOffers(self.address.address, next_order_id).call(block_identifier=block_ident) | |
else: | |
break | |
else: | |
order_id = self._contract.functions.getBestOffer(pay_token.address, buy_token.address).call(block_identifier=block_ident) | |
while order_id != 0: | |
order = self.get_order(order_id, block_ident) | |
if order is not None: | |
orders.append(order) | |
order_id = self._contract.functions.getWorseOffer(order_id).call(block_identifier=block_ident) | |
return sorted(orders, key=lambda order: order.order_id) | |
else: | |
return super(MatchingMarket, self).get_orders(pay_token, buy_token) | |
def make(self, p_token: Token, pay_amount: Wad, b_token: Token, buy_amount: Wad, pos: int = None) -> Transact: | |
"""Create a new order. | |
The `have_amount` of `have_token` token will be taken from you on order 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 orders, which allows the contract | |
to do automated matching. Client placing a new order 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 order | |
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. | |
When complete, `receipt.result` will contain order_id of the new order. | |
Args: | |
p_token: Token object (see `model.py`) of the ERC20 token you want to put on sale. | |
pay_amount: Amount of the `pay_token` token you want to put on sale. | |
b_token: Token object (see `model.py`) of the ERC20 token you want to be paid with. | |
buy_amount: Amount of the `buy_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:`pymaker.Transact` instance, which can be used to trigger the transaction. | |
""" | |
assert(isinstance(p_token, Token)) | |
assert(isinstance(pay_amount, Wad)) | |
assert(isinstance(b_token, Token)) | |
assert(isinstance(buy_amount, Wad)) | |
assert(isinstance(pos, int) or (pos is None)) | |
assert(pay_amount > Wad(0)) | |
assert(buy_amount > Wad(0)) | |
pay_token = p_token.address | |
buy_token = b_token.address | |
if pos is None: | |
pos = self.position(pay_amount=pay_amount, | |
p_token=p_token, | |
b_token=b_token, | |
buy_amount=buy_amount) | |
else: | |
assert(pos >= 0) | |
return Transact(self, self.web3, self.abi, self.address, self._contract, | |
'offer(uint256,address,uint256,address,uint256)', | |
[pay_amount.value, pay_token.address, buy_amount.value, buy_token.address, pos], None, | |
self._make_order_id_result_function) | |
def position(self, p_token: Token, pay_amount: Wad, b_token: Token, buy_amount: Wad) -> int: | |
"""Calculate the position (`pos`) new order should be inserted at to minimize gas costs. | |
The `MatchingMarket` contract maintains an internal ordered linked list of orders, which allows the contract | |
to do automated matching. Client placing a new order 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 order | |
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: | |
p_token: Token object (see `model.py`) of the token you want to put on sale. | |
pay_amount: Amount of the `pay_token` token you want to put on sale. | |
b_token: Token object (see `model.py`) of the token you want to be paid with. | |
buy_amount: Amount of the `buy_token` you want to receive. | |
Returns: | |
The position (`pos`) new order should be inserted at. | |
""" | |
assert(isinstance(p_token, Token)) | |
assert(isinstance(pay_amount, Wad)) | |
assert(isinstance(b_token, Token)) | |
assert(isinstance(buy_amount, Wad)) | |
pay_token = p_token.address | |
buy_token = b_token.address | |
self.logger.debug("Enumerating orders for position calculation...") | |
orders = filter(lambda order: order.pay_amount / order.buy_amount >= p_token.normalize_amount(pay_amount) / b_token.normalize_amount(buy_amount), | |
self.get_orders(p_token, b_token)) | |
self.logger.debug("Enumerating orders for position calculation finished") | |
sorted_orders = sorted(orders, key=lambda o: o.pay_amount / o.buy_amount) | |
return sorted_orders[0].order_id if len(sorted_orders) > 0 else 0 | |
def __repr__(self): | |
return f"MatchingMarket('{self.address}')" |