class Flipper(AuctionContract): """A client for the `Flipper` contract, used to interact with collateral auctions. You can find the source code of the `Flipper` contract here: <https://github.com/makerdao/dss/blob/master/src/flip.sol>. Attributes: web3: An instance of `Web` from `web3.py`. address: Ethereum address of the `Flipper` contract. Event signatures: 0x65fae35e: (deployment-related) 0x9c52a7f1: (deployment-related) 0x29ae8114: file 0xc84ce3a1172f0dec3173f04caaa6005151a4bfe40d4c9f3ea28dba5f719b2a7a: kick 0x4b43ed12: tend 0x5ff3a382: dent 0xc959c42b: deal """ abi = Contract._load_abi(__name__, 'abi/Flipper.abi') bin = Contract._load_bin(__name__, 'abi/Flipper.bin') class Bid: def __init__(self, id: int, bid: Rad, lot: Wad, guy: Address, tic: int, end: int, usr: Address, gal: Address, tab: Rad): assert(isinstance(id, int)) assert(isinstance(bid, Rad)) assert(isinstance(lot, Wad)) assert(isinstance(guy, Address)) assert(isinstance(tic, int)) assert(isinstance(end, int)) assert(isinstance(usr, Address)) assert(isinstance(gal, Address)) assert(isinstance(tab, Rad)) self.id = id self.bid = bid self.lot = lot self.guy = guy self.tic = tic self.end = end self.usr = usr self.gal = gal self.tab = tab def __repr__(self): return f"Flipper.Bid({pformat(vars(self))})" class KickLog: def __init__(self, log): args = log['args'] self.id = args['id'] self.lot = Wad(args['lot']) self.bid = Rad(args['bid']) self.tab = Rad(args['tab']) self.usr = Address(args['usr']) self.gal = Address(args['gal']) self.block = log['blockNumber'] self.tx_hash = log['transactionHash'].hex() def __repr__(self): return f"Flipper.KickLog({pformat(vars(self))})" class TendLog: def __init__(self, lognote: LogNote): self.guy = Address(lognote.usr) self.id = Web3.toInt(lognote.arg1) self.lot = Wad(Web3.toInt(lognote.arg2)) self.bid = Rad(Web3.toInt(lognote.get_bytes_at_index(2))) self.block = lognote.block self.tx_hash = lognote.tx_hash def __repr__(self): return f"Flipper.TendLog({pformat(vars(self))})" class DentLog: def __init__(self, lognote: LogNote): self.guy = Address(lognote.usr) self.id = Web3.toInt(lognote.arg1) self.lot = Wad(Web3.toInt(lognote.arg2)) self.bid = Rad(Web3.toInt(lognote.get_bytes_at_index(2))) self.block = lognote.block self.tx_hash = lognote.tx_hash def __repr__(self): return f"Flipper.DentLog({pformat(vars(self))})" def __init__(self, web3: Web3, address: Address): super(Flipper, self).__init__(web3, address, Flipper.abi, self.bids) def bids(self, id: int) -> Bid: """Returns the auction details. Args: id: Auction identifier. Returns: The auction details. """ assert(isinstance(id, int)) array = self._contract.functions.bids(id).call() return Flipper.Bid(id=id, bid=Rad(array[0]), lot=Wad(array[1]), guy=Address(array[2]), tic=int(array[3]), end=int(array[4]), usr=Address(array[5]), gal=Address(array[6]), tab=Rad(array[7])) def kick(self, usr: Address, gal: Address, tab: Rad, lot: Wad, bid: Rad) -> Transact: assert(isinstance(usr, Address)) assert(isinstance(gal, Address)) assert(isinstance(tab, Rad)) assert(isinstance(lot, Wad)) assert(isinstance(bid, Rad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'kick', [usr.address, gal.address, tab.value, lot.value, bid.value]) def tend(self, id: int, lot: Wad, bid: Rad) -> Transact: assert(isinstance(id, int)) assert(isinstance(lot, Wad)) assert(isinstance(bid, Rad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'tend', [id, lot.value, bid.value]) def dent(self, id: int, lot: Wad, bid: Rad) -> Transact: assert(isinstance(id, int)) assert(isinstance(lot, Wad)) assert(isinstance(bid, Rad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'dent', [id, lot.value, bid.value]) def past_logs(self, from_block: int, to_block: int = None): logs = super().get_past_lognotes(Flipper.abi, from_block, to_block) history = [] for log in logs: if log is None: continue elif isinstance(log, Flipper.KickLog): history.append(log) elif log.sig == '0x4b43ed12': history.append(Flipper.TendLog(log)) elif log.sig == '0x5ff3a382': history.append(Flipper.DentLog(log)) elif log.sig == '0xc959c42b': history.append(AuctionContract.DealLog(log)) return history def parse_event(self, event): signature = Web3.toHex(event['topics'][0]) codec = ABICodec(default_registry) if signature == "0xc84ce3a1172f0dec3173f04caaa6005151a4bfe40d4c9f3ea28dba5f719b2a7a": event_data = get_event_data(codec, self.kick_abi, event) return Flipper.KickLog(event_data) else: event_data = get_event_data(codec, self.log_note_abi, event) return LogNote(event_data) def __repr__(self): return f"Flipper('{self.address}')"
class TestDynamicGasPrice: router_abi = Contract._load_abi( __name__, '../lib/pyexchange/pyexchange/abi/UniswapV2Router02.abi') router_bin = Contract._load_bin( __name__, '../lib/pyexchange/pyexchange/abi/UniswapV2Router02.bin') factory_abi = Contract._load_abi( __name__, '../lib/pyexchange/pyexchange/abi/UniswapV2Factory.abi') factory_bin = Contract._load_bin( __name__, '../lib/pyexchange/pyexchange/abi/UniswapV2Factory.bin') weth_abi = Contract._load_abi(__name__, '../lib/pyexchange/pyexchange/abi/WETH.abi') weth_bin = Contract._load_bin(__name__, '../lib/pyexchange/pyexchange/abi/WETH.bin') def setup_method(self): # Use Ganache docker container self.web3 = Web3(HTTPProvider("http://0.0.0.0:8555")) self.web3.eth.defaultAccount = Web3.toChecksumAddress( "0x9596C16D7bF9323265C2F2E22f43e6c80eB3d943") self.our_address = Address(self.web3.eth.defaultAccount) self.private_key = "0x91cf2cc3671a365fcbf38010ff97ee31a5b7e674842663c56769e41600696ead" register_private_key(self.web3, self.private_key) self.weth_address = Contract._deploy(self.web3, self.weth_abi, self.weth_bin, []) self.factory_address = Contract._deploy(self.web3, self.factory_abi, self.factory_bin, [self.our_address.address]) self.router_address = Contract._deploy( self.web3, self.router_abi, self.router_bin, [self.factory_address.address, self.weth_address.address]) self._weth_contract = Contract._get_contract(self.web3, self.weth_abi, self.weth_address) self.ds_dai = DSToken.deploy(self.web3, 'DAI') self.ds_dai.mint( Wad.from_number(500)).transact(from_address=self.our_address) self.token_dai = Token("DAI", self.ds_dai.address, 18) self.token_weth = Token("WETH", self.weth_address, 18) token_config = { "tokens": { "DAI": { "tokenAddress": self.ds_dai.address.address }, "WETH": { "tokenAddress": self.weth_address.address } } } # write token config with locally deployed addresses to file with open("test-token-config.json", "w+") as outfile: outfile.write(json.dumps(token_config)) def get_target_balances(self, pair: str) -> dict: assert (isinstance(pair, str)) formatted_pair = "_".join(pair.split("-")).upper() token_a = formatted_pair.split("_")[0] token_b = formatted_pair.split("_")[1] return { "min_a": TARGET_AMOUNTS[f"{formatted_pair}_MIN_{token_a}"], "max_a": TARGET_AMOUNTS[f"{formatted_pair}_MAX_{token_a}"], "min_b": TARGET_AMOUNTS[f"{formatted_pair}_MIN_{token_b}"], "max_b": TARGET_AMOUNTS[f"{formatted_pair}_MAX_{token_b}"] } def instantiate_uniswap_keeper_using_dynamic_gas( self, pair: str) -> UniswapV2MarketMakerKeeper: if pair == "DAI-USDC": feed_price = "fixed:1.01" elif pair == "ETH-DAI": feed_price = "fixed:420" target_balances = self.get_target_balances(pair) return UniswapV2MarketMakerKeeper(args=args( f"--eth-from {self.our_address} --endpoint-uri http://localhost:8555" f" --eth-key {self.private_key}" f" --pair {pair}" f" --accepted-price-slippage-up 50" f" --accepted-price-slippage-down 30" f" --target-a-min-balance {target_balances['min_a']}" f" --target-a-max-balance {target_balances['max_a']}" f" --target-b-min-balance {target_balances['min_b']}" f" --target-b-max-balance {target_balances['max_b']}" f" --token-config ./test-token-config.json" f" --router-address {self.router_address.address}" f" --factory-address {self.factory_address.address}" f" --initial-delay 3" f" --dynamic-gas-price" f" --oracle-gas-price" f" --price-feed {feed_price}"), web3=self.web3) def instantiate_uniswap_keeper_using_fixed_gas( self, pair: str) -> UniswapV2MarketMakerKeeper: if pair == "DAI-USDC": feed_price = "fixed:1.01" elif pair == "ETH-DAI": feed_price = "fixed:420" target_balances = self.get_target_balances(pair) return UniswapV2MarketMakerKeeper(args=args( f"--eth-from {self.our_address} --endpoint-uri http://localhost:8555" f" --eth-key {self.private_key}" f" --pair {pair}" f" --accepted-price-slippage-up 50" f" --accepted-price-slippage-down 30" f" --target-a-min-balance {target_balances['min_a']}" f" --target-a-max-balance {target_balances['max_a']}" f" --target-b-min-balance {target_balances['min_b']}" f" --target-b-max-balance {target_balances['max_b']}" f" --token-config ./test-token-config.json" f" --router-address {self.router_address.address}" f" --factory-address {self.factory_address.address}" f" --initial-delay 3" f" --dynamic-gas-price" f" --fixed-gas-price 20" f" --price-feed {feed_price}"), web3=self.web3) def test_dynamic_oracle_gas_uniswap(self): keeper = self.instantiate_uniswap_keeper_using_dynamic_gas('ETH-DAI') assert isinstance(keeper.gas_price.gas_station, Aggregator) assert keeper.gas_price.gas_station.URL == "aggregator" assert isinstance(keeper.gas_price, DynamicGasPrice) def test_dynamic_fixed_gas_uniswap(self): keeper = self.instantiate_uniswap_keeper_using_dynamic_gas('ETH-DAI') assert keeper.gas_price.get_gas_price(0) == 20000000000 assert isinstance(keeper.gas_price, DynamicGasPrice)
class Flopper(Contract): """A client for the `Flopper` contract, TODO. You can find the source code of the `Flopper` contract here: <https://github.com/makerdao/dss/blob/master/src/flop.sol>. Attributes: web3: An instance of `Web` from `web3.py`. address: Ethereum address of the `Flopper` contract. """ abi = Contract._load_abi(__name__, 'abi/Flopper.abi') bin = Contract._load_bin(__name__, 'abi/Flopper.bin') class Bid: def __init__(self, bid: Wad, lot: Wad, guy: Address, tic: int, end: int, vow: Address): assert (isinstance(bid, Wad)) assert (isinstance(lot, Wad)) assert (isinstance(guy, Address)) assert (isinstance(tic, int)) assert (isinstance(end, int)) assert (isinstance(vow, Address)) self.bid = bid self.lot = lot self.guy = guy self.tic = tic self.end = end self.vow = vow @staticmethod def deploy(web3: Web3, dai: Address, gem: Address): assert (isinstance(dai, Address)) assert (isinstance(gem, Address)) return Flopper(web3=web3, address=Contract._deploy(web3, Flopper.abi, Flopper.bin, [dai.address, gem.address])) 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) def rely(self, guy: Address) -> Transact: assert isinstance(guy, Address) return Transact(self, self.web3, self.abi, self.address, self._contract, 'rely', [guy.address]) def approve(self, approval_function): """Approve the `Flapper` to access our `gem` so we can participate in auctions. For available approval functions (i.e. approval modes) see `directly` and `via_tx_manager` in `pymaker.approval`. Args: approval_function: Approval function (i.e. approval mode). """ assert (callable(approval_function)) approval_function(ERC20Token(web3=self.web3, address=self.dai()), self.address, 'Flopper') def dai(self) -> Address: """Returns the `dai` token. Returns: The address of the `dai` token. """ return Address(self._contract.call().dai()) def gem(self) -> Address: """Returns the `gem` token. Returns: The address of the `gem` token. """ return Address(self._contract.call().gem()) def beg(self) -> Ray: """Returns the percentage minimum bid increase. Returns: The percentage minimum bid increase. """ return Ray(self._contract.call().beg()) def ttl(self) -> int: """Returns the bid lifetime. Returns: The bid lifetime (in seconds). """ return int(self._contract.call().ttl()) def tau(self) -> int: """Returns the total auction length. Returns: The total auction length (in seconds). """ return int(self._contract.call().tau()) def kicks(self) -> int: """Returns the number of auctions started so far. Returns: The number of auctions started so far. """ return int(self._contract.call().kicks()) def bids(self, id: int) -> Bid: """Returns the auction details. Args: id: Auction identifier. Returns: The auction details. """ assert (isinstance(id, int)) array = self._contract.call().bids(id) return Flopper.Bid(bid=Wad(array[0]), lot=Wad(array[1]), guy=Address(array[2]), tic=int(array[3]), end=int(array[4]), vow=Address(array[5])) def kick(self, gal: Address, lot: Wad, bid: Wad) -> Transact: assert (isinstance(gal, Address)) assert (isinstance(lot, Wad)) assert (isinstance(bid, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'kick', [gal.address, lot.value, bid.value]) def dent(self, id: int, lot: Wad, bid: Wad) -> Transact: assert (isinstance(id, int)) assert (isinstance(lot, Wad)) assert (isinstance(bid, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'dent', [id, lot.value, bid.value]) def deal(self, id: int) -> Transact: assert (isinstance(id, int)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'deal', [id]) def __repr__(self): return f"Flopper('{self.address}')"
class MatchingMarket(ExpiringMarket): """A client for a `MatchingMarket` contract. You can find the source code of the `OasisDEX` contracts here: <https://github.com/makerdao/maker-otc>. Attributes: web3: An instance of `Web` from `web3.py`. address: Ethereum address of the `MatchingMarket` contract. 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, close_time: int, support_address: Optional[Address] = None): """Deploy a new instance of the `MatchingMarket` contract. Args: web3: An instance of `Web` from `web3.py`. close_time: Unix timestamp of when the market will close. support_address: Ethereum address of the `MakerOtcSupportMethods` contract (optional). Returns: A `MatchingMarket` class instance. """ return MatchingMarket(web3=web3, address=Contract._deploy(web3, MatchingMarket.abi, MatchingMarket.bin, [close_time]), support_address=support_address) def is_buy_enabled(self) -> bool: """Checks if direct buy is enabled. Returns: `True` if direct buy is enabled, `False` otherwise. """ return self._contract.call().buyEnabled() def set_buy_enabled(self, buy_enabled: bool) -> Transact: """Enables or disables direct buy. Args: buy_enabled: Whether direct buy should be enabled or disabled. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(buy_enabled, bool)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'setBuyEnabled', [buy_enabled]) def is_matching_enabled(self) -> bool: """Checks if order matching is enabled. Returns: `True` if order matching is enabled, `False` otherwise. """ return self._contract.call().matchingEnabled() def set_matching_enabled(self, matching_enabled: bool) -> Transact: """Enables or disables order matching. Args: matching_enabled: Whether order matching should be enabled or disabled. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(matching_enabled, bool)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'setMatchingEnabled', [matching_enabled]) def add_token_pair_whitelist(self, base_token: Address, quote_token: Address) -> Transact: """Adds a token pair to the whitelist. All 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, 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. 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: `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)) if pay_token is not None and buy_token is not None: orders = [] if self._support_contract: result = self._support_contract.call().getOffers( self.address.address, pay_token.address, buy_token.address) 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=Wad(result[1][i]), buy_token=buy_token, buy_amount=Wad(result[2][i]), timestamp=result[4][i])) if count == 100: next_order_id = self._contract.call().getWorseOffer( orders[-1].order_id) result = self._support_contract.call().getOffers( self.address.address, next_order_id) else: break else: order_id = self._contract.call().getBestOffer( pay_token.address, buy_token.address) while order_id != 0: order = self.get_order(order_id) if order is not None: orders.append(order) order_id = self._contract.call().getWorseOffer(order_id) return sorted(orders, key=lambda order: order.order_id) else: return super(ExpiringMarket, self).get_orders(pay_token, buy_token) def make(self, pay_token: Address, pay_amount: Wad, buy_token: Address, 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: 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. 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(pay_token, Address)) assert (isinstance(pay_amount, Wad)) assert (isinstance(buy_token, Address)) assert (isinstance(buy_amount, Wad)) assert (isinstance(pos, int) or (pos is None)) assert (pay_amount > Wad(0)) assert (buy_amount > Wad(0)) if pos is None: pos = self.position(pay_token=pay_token, pay_amount=pay_amount, buy_token=buy_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, pay_token: Address, pay_amount: Wad, buy_token: Address, 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: 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: The position (`pos`) new order should be inserted at. """ assert (isinstance(pay_token, Address)) assert (isinstance(pay_amount, Wad)) assert (isinstance(buy_token, Address)) assert (isinstance(buy_amount, Wad)) self.logger.debug("Enumerating orders for position calculation...") orders = filter( lambda order: order.pay_amount / order.buy_amount >= pay_amount / buy_amount, self.get_orders(pay_token, buy_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}')"
class TestUniswapV2MarketMakerKeeper: router_abi = Contract._load_abi(__name__, '../lib/pyexchange/pyexchange/abi/UniswapV2Router02.abi') router_bin = Contract._load_bin(__name__, '../lib/pyexchange/pyexchange/abi/UniswapV2Router02.bin') factory_abi = Contract._load_abi(__name__, '../lib/pyexchange/pyexchange/abi/UniswapV2Factory.abi') factory_bin = Contract._load_bin(__name__, '../lib/pyexchange/pyexchange/abi/UniswapV2Factory.bin') weth_abi = Contract._load_abi(__name__, '../lib/pyexchange/pyexchange/abi/WETH.abi') weth_bin = Contract._load_bin(__name__, '../lib/pyexchange/pyexchange/abi/WETH.bin') uni_staking_rewards_abi = Contract._load_abi(__name__, '../lib/pyexchange/pyexchange/abi/UniStakingRewards.abi')['abi'] uni_staking_rewards_bin = Contract._load_bin(__name__, '../lib/pyexchange/pyexchange/abi/UniStakingRewards.bin') logger = logging.getLogger() def setup_method(self): # Use Ganache docker container self.web3 = Web3(HTTPProvider("http://0.0.0.0:8555")) self.web3.eth.defaultAccount = Web3.toChecksumAddress("0x9596C16D7bF9323265C2F2E22f43e6c80eB3d943") self.our_address = Address(self.web3.eth.defaultAccount) self.private_key = "0x91cf2cc3671a365fcbf38010ff97ee31a5b7e674842663c56769e41600696ead" register_private_key(self.web3, self.private_key) self.weth_address = Contract._deploy(self.web3, self.weth_abi, self.weth_bin, []) self.factory_address = Contract._deploy(self.web3, self.factory_abi, self.factory_bin, [self.our_address.address]) self.router_address = Contract._deploy(self.web3, self.router_abi, self.router_bin, [self.factory_address.address, self.weth_address.address]) self._weth_contract = Contract._get_contract(self.web3, self.weth_abi, self.weth_address) self.deploy_tokens() token_config = { "tokens": { "DAI": { "tokenAddress": self.ds_dai.address.address }, "KEEP": { "tokenAddress": self.ds_keep.address.address }, "LEV": { "tokenAddress": self.ds_lev.address.address, "tokenDecimals": 9 }, "USDC": { "tokenAddress": self.ds_usdc.address.address, "tokenDecimals": 6 }, "WBTC": { "tokenAddress": self.ds_wbtc.address.address, "tokenDecimals": 8 }, "WETH": { "tokenAddress": self.weth_address.address } } } # write token config with locally deployed addresses to file with open("test-token-config.json", "w+") as outfile: outfile.write(json.dumps(token_config)) def deploy_tokens(self): self.ds_dai = DSToken.deploy(self.web3, 'DAI') self.ds_keep = DSToken.deploy(self.web3, 'KEEP') self.ds_lev = DSToken.deploy(self.web3, 'LEV') self.ds_usdc = DSToken.deploy(self.web3, 'USDC') self.ds_wbtc = DSToken.deploy(self.web3, 'WBTC') self.token_dai = Token("DAI", self.ds_dai.address, 18) self.token_keep = Token("KEEP", self.ds_keep.address, 18) self.token_lev = Token("LEV", self.ds_lev.address, 9) self.token_usdc = Token("USDC", self.ds_usdc.address, 6) self.token_wbtc = Token("WBTC", self.ds_wbtc.address, 8) self.token_weth = Token("WETH", self.weth_address, 18) def deploy_staking_rewards(self, liquidity_token_address: Address): self.ds_reward_dai = DSToken.deploy(self.web3, 'REWARD_DAI') self.reward_token = Token("REWARD_DAI", self.ds_dai.address, 18) self.uni_staking_rewards_address = Contract._deploy(self.web3, self.uni_staking_rewards_abi, self.uni_staking_rewards_bin, [self.our_address.address, self.reward_token.address.address, liquidity_token_address.address]) self.uni_staking_rewards = UniswapStakingRewards(self.web3, self.our_address, Address(self.uni_staking_rewards_address), "UniswapStakingRewards") def mint_tokens(self): self.ds_dai.mint(Wad.from_number(500)).transact(from_address=self.our_address) self.ds_keep.mint(Wad.from_number(5000)).transact(from_address=self.our_address) self.ds_usdc.mint(self.token_usdc.unnormalize_amount(Wad.from_number(505))).transact(from_address=self.our_address) self.ds_wbtc.mint(self.token_wbtc.unnormalize_amount(Wad.from_number(15))).transact(from_address=self.our_address) def get_target_balances(self, pair: str) -> dict: assert (isinstance(pair, str)) formatted_pair = "_".join(pair.split("-")).upper() token_a = formatted_pair.split("_")[0] token_b = formatted_pair.split("_")[1] return { "min_a": TARGET_AMOUNTS[f"{formatted_pair}_MIN_{token_a}"], "max_a": TARGET_AMOUNTS[f"{formatted_pair}_MAX_{token_a}"], "min_b": TARGET_AMOUNTS[f"{formatted_pair}_MIN_{token_b}"], "max_b": TARGET_AMOUNTS[f"{formatted_pair}_MAX_{token_b}"] } def instantiate_keeper(self, pair: str) -> UniswapV2MarketMakerKeeper: if pair == "DAI-USDC": feed_price = "fixed:1.01" elif pair == "ETH-DAI": feed_price = "fixed:420" elif pair == "WBTC-USDC": feed_price = "fixed:12000" elif pair == "KEEP-ETH": feed_price = "fixed:0.00291025" elif pair == "LEV-ETH": feed_price = "fixed:0.00024496" target_balances = self.get_target_balances(pair) return UniswapV2MarketMakerKeeper(args=args(f"--eth-from {self.our_address} --endpoint-uri http://localhost:8555" f" --eth-key {self.private_key}" f" --pair {pair}" f" --accepted-price-slippage-up 50" f" --accepted-price-slippage-down 30" f" --target-a-min-balance {target_balances['min_a']}" f" --target-a-max-balance {target_balances['max_a']}" f" --target-b-min-balance {target_balances['min_b']}" f" --target-b-max-balance {target_balances['max_b']}" f" --token-config ./test-token-config.json" f" --router-address {self.router_address.address}" f" --factory-address {self.factory_address.address}" f" --initial-delay 3" f" --price-feed {feed_price}"), web3=self.web3) def test_should_stake_liquidity(self): # given self.mint_tokens() keeper = self.instantiate_keeper("ETH-DAI") dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) eth_balance = keeper.uniswap.get_account_eth_balance() # when keeper_thread = threading.Thread(target=keeper.main, daemon=True).start() time.sleep(10) # then final_dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) final_eth_balance = keeper.uniswap.get_account_eth_balance() assert dai_balance > final_dai_balance assert eth_balance > final_eth_balance # when self.deploy_staking_rewards(keeper.uniswap.pair_address) staking_rewards_contract_address = self.uni_staking_rewards_address staking_rewards_args = Namespace(eth_from=self.our_address, staking_rewards_name=StakingRewardsName.UNISWAP_STAKING_REWARDS, staking_rewards_contract_address=self.uni_staking_rewards_address) keeper.staking_rewards = StakingRewardsFactory().create_staking_rewards(staking_rewards_args, self.web3) keeper.staking_rewards.approve(keeper.uniswap.pair_address) # when REMOVE LIQUIDITY TO READD keeper.testing_feed_price = True keeper.test_price = Wad.from_number(PRICES.ETH_DAI_REMOVE_LIQUIDITY.value) time.sleep(10) keeper.testing_feed_price = True keeper.test_price = Wad.from_number(PRICES.ETH_DAI_ADD_LIQUIDITY.value) time.sleep(10) # then staked_liquidity_balance = keeper.staking_rewards.balance_of() assert staked_liquidity_balance > Wad(0) def test_should_withdraw_liquidity(self): # given self.mint_tokens() keeper = self.instantiate_keeper("ETH-DAI") dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) eth_balance = keeper.uniswap.get_account_eth_balance() # when keeper_thread = threading.Thread(target=keeper.main, daemon=True).start() time.sleep(10) # then final_dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) final_eth_balance = keeper.uniswap.get_account_eth_balance() assert dai_balance > final_dai_balance assert eth_balance > final_eth_balance # when self.deploy_staking_rewards(keeper.uniswap.pair_address) staking_rewards_contract_address = self.uni_staking_rewards_address staking_rewards_args = Namespace(eth_from=self.our_address, staking_rewards_name=StakingRewardsName.UNISWAP_STAKING_REWARDS, staking_rewards_contract_address=self.uni_staking_rewards_address) keeper.staking_rewards = StakingRewardsFactory().create_staking_rewards(staking_rewards_args, self.web3) keeper.staking_rewards.approve(keeper.uniswap.pair_address) # when REMOVE LIQUIDITY TO READD keeper.testing_feed_price = True keeper.test_price = Wad.from_number(PRICES.ETH_DAI_REMOVE_LIQUIDITY.value) time.sleep(10) keeper.testing_feed_price = True keeper.test_price = Wad.from_number(PRICES.ETH_DAI_ADD_LIQUIDITY.value) time.sleep(10) # then staked_liquidity_balance = keeper.staking_rewards.balance_of() assert staked_liquidity_balance > Wad(0) keeper.testing_feed_price = True keeper.test_price = Wad.from_number(PRICES.ETH_DAI_REMOVE_LIQUIDITY.value) time.sleep(10) staked_liquidity_balance = keeper.staking_rewards.balance_of() assert staked_liquidity_balance == Wad(0) def test_calculate_token_liquidity_to_add(self): # given self.mint_tokens() keeper = self.instantiate_keeper("DAI-USDC") keeper.uniswap_current_exchange_price = Wad.from_number(PRICES.DAI_USDC_ADD_LIQUIDITY.value) # when dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) usdc_balance = keeper.uniswap.get_account_token_balance(self.token_usdc) liquidity_to_add = keeper.calculate_liquidity_args(dai_balance, usdc_balance) # then assert all(map(lambda x: x > Wad(0), liquidity_to_add.values())) assert liquidity_to_add['amount_a_desired'] > liquidity_to_add['amount_a_min'] assert liquidity_to_add['amount_b_desired'] > liquidity_to_add['amount_b_min'] def test_calculate_eth_liquidity_to_add(self): # given self.mint_tokens() keeper = self.instantiate_keeper("ETH-DAI") keeper.uniswap_current_exchange_price = Wad.from_number(PRICES.ETH_DAI_ADD_LIQUIDITY.value) # when dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) eth_balance = keeper.uniswap.get_account_eth_balance() liquidity_to_add = keeper.calculate_liquidity_args(eth_balance, dai_balance) # then assert all(map(lambda x: x > Wad(0), liquidity_to_add.values())) assert liquidity_to_add['amount_b_desired'] > liquidity_to_add['amount_b_min'] assert liquidity_to_add['amount_a_desired'] > liquidity_to_add['amount_a_min'] def test_should_ensure_adequate_eth_for_gas(self): # given self.mint_tokens() keeper = self.instantiate_keeper("ETH-DAI") # when dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) liquidity_to_add = keeper.calculate_liquidity_args(Wad.from_number(0.5), dai_balance) # then assert liquidity_to_add is None def test_should_determine_add_liquidity(self): # given self.mint_tokens() keeper = self.instantiate_keeper("DAI-USDC") # when add_liquidity, remove_liquidity = keeper.determine_liquidity_action() # then assert add_liquidity == True assert remove_liquidity == False def test_should_add_dai_usdc_liquidity(self): # given self.mint_tokens() keeper = self.instantiate_keeper("DAI-USDC") initial_dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) initial_usdc_balance = keeper.uniswap.get_account_token_balance(self.token_usdc) # when keeper_thread = threading.Thread(target=keeper.main, daemon=True).start() time.sleep(10) added_liquidity = keeper.calculate_liquidity_args(initial_dai_balance, initial_usdc_balance) # then exchange_dai_balance = keeper.uniswap.get_exchange_balance(self.token_dai, keeper.uniswap.pair_address) exchange_usdc_balance = keeper.uniswap.get_exchange_balance(self.token_usdc, keeper.uniswap.pair_address) final_dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) final_usdc_balance = keeper.uniswap.get_account_token_balance(self.token_usdc) assert keeper.uniswap.get_our_exchange_balance(self.token_usdc, keeper.uniswap.pair_address) > Wad.from_number(0) assert keeper.uniswap.get_our_exchange_balance(self.token_dai, keeper.uniswap.pair_address) > Wad.from_number(0) assert initial_dai_balance > final_dai_balance assert initial_usdc_balance > final_usdc_balance assert added_liquidity['amount_a_desired'] == exchange_dai_balance assert self.token_usdc.normalize_amount(added_liquidity['amount_b_desired']) == exchange_usdc_balance def test_should_add_wbtc_usdc_liquidity(self): # given self.mint_tokens() keeper = self.instantiate_keeper("WBTC-USDC") initial_wbtc_balance = keeper.uniswap.get_account_token_balance(self.token_wbtc) initial_usdc_balance = keeper.uniswap.get_account_token_balance(self.token_usdc) # when keeper_thread = threading.Thread(target=keeper.main, daemon=True).start() time.sleep(10) added_liquidity = keeper.calculate_liquidity_args(initial_wbtc_balance, initial_usdc_balance) # then exchange_wbtc_balance = keeper.uniswap.get_exchange_balance(self.token_wbtc, keeper.uniswap.pair_address) exchange_usdc_balance = keeper.uniswap.get_exchange_balance(self.token_usdc, keeper.uniswap.pair_address) final_wbtc_balance = keeper.uniswap.get_account_token_balance(self.token_wbtc) final_usdc_balance = keeper.uniswap.get_account_token_balance(self.token_usdc) assert initial_wbtc_balance > final_wbtc_balance assert initial_usdc_balance > final_usdc_balance assert self.token_wbtc.normalize_amount(added_liquidity['amount_a_desired']) == exchange_wbtc_balance assert self.token_usdc.normalize_amount(added_liquidity['amount_b_desired']) == exchange_usdc_balance def test_should_add_dai_eth_liquidity(self): # given self.mint_tokens() keeper = self.instantiate_keeper("ETH-DAI") dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) eth_balance = keeper.uniswap.get_account_eth_balance() # when keeper_thread = threading.Thread(target=keeper.main, daemon=True).start() time.sleep(12) # then final_dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) final_eth_balance = keeper.uniswap.get_account_eth_balance() assert dai_balance > final_dai_balance assert eth_balance > final_eth_balance # gas usage breaks eth_balance assertion assert keeper.uniswap.get_our_exchange_balance(self.token_dai, keeper.uniswap.pair_address) > Wad.from_number(0) assert keeper.uniswap.get_our_exchange_balance(self.token_weth, keeper.uniswap.pair_address) > Wad.from_number(0) def test_should_remove_dai_usdc_liquidity(self): # given self.mint_tokens() keeper = self.instantiate_keeper("DAI-USDC") initial_dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) initial_usdc_balance = keeper.uniswap.get_account_token_balance(self.token_usdc) # when keeper_thread = threading.Thread(target=keeper.main, daemon=True).start() time.sleep(10) added_liquidity = keeper.calculate_liquidity_args(initial_dai_balance, initial_usdc_balance) post_add_exchange_dai_balance = keeper.uniswap.get_exchange_balance(self.token_dai, keeper.uniswap.pair_address) post_add_exchange_usdc_balance = keeper.uniswap.get_exchange_balance(self.token_usdc, keeper.uniswap.pair_address) post_add_dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) post_add_usdc_balance = keeper.uniswap.get_account_token_balance(self.token_usdc) assert initial_dai_balance > post_add_dai_balance assert initial_usdc_balance > post_add_usdc_balance assert added_liquidity['amount_a_desired'] == post_add_exchange_dai_balance assert self.token_usdc.normalize_amount(added_liquidity['amount_b_desired']) == post_add_exchange_usdc_balance keeper.testing_feed_price = True keeper.test_price = Wad.from_number(PRICES.DAI_USDC_REMOVE_LIQUIDITY.value) time.sleep(10) post_remove_exchange_dai_balance = keeper.uniswap.get_exchange_balance(self.token_dai, keeper.uniswap.pair_address) post_remove_exchange_usdc_balance = keeper.uniswap.get_exchange_balance(self.token_usdc, keeper.uniswap.pair_address) post_remove_dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) post_remove_usdc_balance = keeper.uniswap.get_account_token_balance(self.token_usdc) assert post_add_exchange_dai_balance > post_remove_exchange_dai_balance assert post_add_exchange_usdc_balance > post_remove_exchange_usdc_balance assert post_remove_dai_balance > post_add_dai_balance assert post_remove_usdc_balance > post_add_usdc_balance def test_should_remove_dai_eth_liquidity(self): # given self.mint_tokens() keeper = self.instantiate_keeper("ETH-DAI") initial_dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) initial_eth_balance = keeper.uniswap.get_account_eth_balance() # when keeper_thread = threading.Thread(target=keeper.main, daemon=True).start() time.sleep(10) # then post_add_dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) post_add_eth_balance = keeper.uniswap.get_account_eth_balance() post_add_exchange_dai_balance = keeper.uniswap.get_our_exchange_balance(self.token_dai, keeper.uniswap.pair_address) post_add_exchange_weth_balance = keeper.uniswap.get_our_exchange_balance(self.token_weth, keeper.uniswap.pair_address) assert initial_dai_balance > post_add_dai_balance assert initial_eth_balance > post_add_eth_balance keeper.testing_feed_price = True keeper.test_price = Wad.from_number(PRICES.ETH_DAI_REMOVE_LIQUIDITY.value) time.sleep(25) post_remove_dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) post_remove_eth_balance = keeper.uniswap.get_account_eth_balance() post_remove_exchange_dai_balance = keeper.uniswap.get_our_exchange_balance(self.token_dai, keeper.uniswap.pair_address) post_remove_exchange_weth_balance = keeper.uniswap.get_exchange_balance(self.token_weth, keeper.uniswap.pair_address) assert post_remove_exchange_dai_balance < post_add_exchange_dai_balance assert post_remove_exchange_weth_balance < post_add_exchange_weth_balance assert post_remove_dai_balance > post_add_dai_balance assert post_remove_eth_balance > post_add_eth_balance def test_should_remove_liquidity_if_price_feed_is_null(self): # given self.mint_tokens() keeper = self.instantiate_keeper("ETH-DAI") initial_dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) initial_eth_balance = keeper.uniswap.get_account_eth_balance() # when keeper_thread = threading.Thread(target=keeper.main, daemon=True).start() time.sleep(10) # then post_add_dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) post_add_eth_balance = keeper.uniswap.get_account_eth_balance() post_add_exchange_dai_balance = keeper.uniswap.get_our_exchange_balance(self.token_dai, keeper.uniswap.pair_address) post_add_exchange_weth_balance = keeper.uniswap.get_our_exchange_balance(self.token_weth, keeper.uniswap.pair_address) assert post_add_exchange_dai_balance > Wad.from_number(0) assert post_add_exchange_weth_balance > Wad.from_number(0) assert initial_dai_balance > post_add_dai_balance assert initial_eth_balance > post_add_eth_balance # when keeper.testing_feed_price = True keeper.test_price = None keeper.price_feed_accepted_delay = 2 time.sleep(25) # then post_remove_dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) post_remove_eth_balance = keeper.uniswap.get_account_eth_balance() post_remove_exchange_dai_balance = keeper.uniswap.get_our_exchange_balance(self.token_dai, keeper.uniswap.pair_address) post_remove_exchange_weth_balance = keeper.uniswap.get_exchange_balance(self.token_weth, keeper.uniswap.pair_address) assert post_remove_exchange_dai_balance < post_add_exchange_dai_balance assert post_remove_exchange_weth_balance < post_add_exchange_weth_balance assert post_remove_dai_balance > post_add_dai_balance assert post_remove_eth_balance > post_add_eth_balance @unittest.skip def test_should_remove_liquidity_if_shutdown_signal_received(self): # given self.mint_tokens() keeper = self.instantiate_keeper("ETH-DAI") initial_dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) initial_eth_balance = keeper.uniswap.get_account_eth_balance() # when # keeper_thread = threading.Thread(target=keeper.main, daemon=True).start() keeper_process = Process(target=keeper.main, daemon=True).start() time.sleep(10) # then post_add_dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) post_add_eth_balance = keeper.uniswap.get_account_eth_balance() post_add_exchange_dai_balance = keeper.uniswap.get_our_exchange_balance(self.token_dai, keeper.uniswap.pair_address) post_add_exchange_weth_balance = keeper.uniswap.get_our_exchange_balance(self.token_weth, keeper.uniswap.pair_address) assert post_add_exchange_dai_balance > Wad.from_number(0) assert post_add_exchange_weth_balance > Wad.from_number(0) assert initial_dai_balance > post_add_dai_balance assert initial_eth_balance > post_add_eth_balance # when # send system interrupt signal to the process and wait for shutdown # pid = os.getpid() pid = keeper_process.current_process().pid os.kill(pid, signal.SIGINT) time.sleep(10) # then post_remove_dai_balance = keeper.uniswap.get_account_token_balance(self.token_dai) post_remove_eth_balance = keeper.uniswap.get_account_eth_balance() post_remove_exchange_dai_balance = keeper.uniswap.get_our_exchange_balance(self.token_dai, keeper.uniswap.pair_address) post_remove_exchange_weth_balance = keeper.uniswap.get_our_exchange_balance(self.token_weth, keeper.uniswap.pair_address) assert post_add_exchange_weth_balance > post_remove_exchange_dai_balance assert post_add_exchange_weth_balance > post_remove_exchange_weth_balance def test_should_remove_liquidity_if_target_amounts_are_breached(self): # given self.mint_tokens() keeper = self.instantiate_keeper("KEEP-ETH") initial_keep_balance = keeper.uniswap.get_account_token_balance(self.token_keep) initial_eth_balance = keeper.uniswap.get_account_eth_balance() # when keeper_thread = threading.Thread(target=keeper.main, daemon=True).start() time.sleep(10) # then post_add_keep_balance = keeper.uniswap.get_account_token_balance(self.token_keep) post_add_eth_balance = keeper.uniswap.get_account_eth_balance() post_add_exchange_keep_balance = keeper.uniswap.get_our_exchange_balance(self.token_keep, keeper.uniswap.pair_address) post_add_exchange_weth_balance = keeper.uniswap.get_our_exchange_balance(self.token_weth, keeper.uniswap.pair_address) assert initial_keep_balance > post_add_keep_balance assert initial_eth_balance > post_add_eth_balance # when # execute a swap that will break the balances target amount and wait for removal eth_to_swap = Wad.from_number(15) min_amount_out = keeper.uniswap.get_amounts_out(eth_to_swap, [self.token_weth, self.token_keep]) keeper.uniswap.swap_exact_eth_for_tokens(eth_to_swap, min_amount_out[1], [self.token_weth.address.address, self.token_keep.address.address]).transact() time.sleep(25) # then post_remove_keep_balance = keeper.uniswap.get_account_token_balance(self.token_keep) post_remove_eth_balance = keeper.uniswap.get_account_eth_balance() assert post_remove_keep_balance > post_add_keep_balance assert post_remove_eth_balance > post_add_eth_balance assert initial_keep_balance > post_remove_keep_balance assert initial_eth_balance > post_remove_eth_balance
class Tap(Contract): """A client for the `Tap` contract. Attributes: web3: An instance of `Web` from `web3.py`. address: Ethereum address of the `Tap` contract. """ abi = Contract._load_abi(__name__, 'abi/SaiTap.abi') bin = Contract._load_bin(__name__, 'abi/SaiTap.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, tub: Address): assert(isinstance(tub, Address)) return Tap(web3=web3, address=Contract._deploy(web3, Tap.abi, Tap.bin, [tub.address])) def set_authority(self, address: Address) -> Transact: assert(isinstance(address, Address)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'setAuthority', [address.address]) def approve(self, approval_function): """Approve the `Tap` to access our SAI, SKR and GEM balances. For available approval functions (i.e. approval modes) see `directly` and `via_tx_manager` in `pymaker.approval`. Args: approval_function: Approval function (i.e. approval mode). """ assert(callable(approval_function)) tub = Tub(web3=self.web3, address=self.tub()) approval_function(ERC20Token(web3=self.web3, address=self.sai()), self.address, 'Tap') approval_function(ERC20Token(web3=self.web3, address=self.skr()), self.address, 'Tap') approval_function(ERC20Token(web3=self.web3, address=tub.gem()), self.address, 'Tap') def tub(self) -> Address: """Get the address of the `Tub` contract. Returns: The address of the `Tub` contract. """ return Address(self._contract.call().tub()) def sai(self) -> Address: """Get the SAI token. Returns: The address of the SAI token. """ return Address(self._contract.call().sai()) def sin(self) -> Address: """Get the SIN token. Returns: The address of the SIN token. """ return Address(self._contract.call().sin()) def skr(self) -> Address: """Get the SKR token. Returns: The address of the SKR token. """ return Address(self._contract.call().skr()) 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 mold_gap(self, new_gap: Wad) -> Transact: """Update the current spread (`gap`) for `boom` and `bust`. Args: new_gap: The new value of the spread (`gap`). `1.0` means no spread, `1.01` means 1% spread. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert isinstance(new_gap, Wad) return Transact(self, self.web3, self.abi, self.address, self._contract, 'mold', ['gap', new_gap.value]) def s2s(self) -> Ray: """Get the current SKR per SAI rate (for `boom` and `bust`). Returns: The current SKR per SAI rate. """ return Ray(self._contract.call().s2s()) def bid(self, amount_in_skr: Wad) -> Wad: """Get the current price of `amount_in_skr` SKR in SAI for `boom`. Returns: The amount in SAI which will be received from `boom` in return of `amount_in_skr` SKR. """ return Wad(self._contract.call().bid(amount_in_skr.value)) def ask(self, amount_in_skr: Wad) -> Wad: """Get the current price of `amount_in_skr` SKR in SAI for `bust`. Returns: The amount in SAI which will be consumed by `bust` if we want to receive `amount_in_skr` SKR from it. """ return Wad(self._contract.call().ask(amount_in_skr.value)) def boom(self, amount_in_skr: Wad) -> Transact: """Buy some amount of SAI to process `joy` (surplus). Args: amount_in_skr: The amount of SKR we want to send in order to receive SAI. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert isinstance(amount_in_skr, Wad) return Transact(self, self.web3, self.abi, self.address, self._contract, 'boom', [amount_in_skr.value]) def bust(self, amount_in_skr: Wad) -> Transact: """Sell some amount of SAI to process `woe` (bad debt). Args: amount_in_skr: The amount of SKR we want to receive in exchange for our SAI. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert isinstance(amount_in_skr, Wad) return Transact(self, self.web3, self.abi, self.address, self._contract, 'bust', [amount_in_skr.value]) def cash(self, amount_in_sai: Wad) -> Transact: """Exchange SAI to GEM after cage. Args: amount_in_sai: The amount of SAI to exchange to GEM. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ return Transact(self, self.web3, self.abi, self.address, self._contract, 'cash', [amount_in_sai.value]) def mock(self, amount_in_sai: Wad) -> Transact: """Exchange GEM to SAI after cage. Args: amount_in_sai: The amount of SAI to buy for GEM. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ return Transact(self, self.web3, self.abi, self.address, self._contract, 'mock', [amount_in_sai.value]) def __eq__(self, other): assert(isinstance(other, Tap)) return self.address == other.address def __repr__(self): return f"Tap('{self.address}')"
def deploy(web3: Web3, per: Ray): assert(isinstance(per, Ray)) return Vox(web3=web3, address=Contract._deploy(web3, Vox.abi, Vox.bin, [per.value]))
def deploy(web3: Web3): return DSValue(web3=web3, address=Contract._deploy(web3, DSValue.abi, DSValue.bin, []))
import time from web3 import Web3, HTTPProvider from pyexchange.uniswapv3 import SwapRouter, PositionManager from pyexchange.uniswapv3_calldata_params import ExactOutputSingleParams, ExactInputSingleParams from pyexchange.uniswapv3_constants import TRADE_TYPE from pyexchange.uniswapv3_entities import Fraction, Route, Trade, CurrencyAmount from pymaker import Address, Contract, Receipt, Transact from pymaker.keys import register_keys, register_private_key from pymaker.model import Token from pymaker.numeric import Wad from pyexchange.uniswapv3_math import encodeSqrtRatioX96 SwapRouter_abi = Contract._load_abi(__name__, '../pyexchange/abi/SwapRouter.abi')['abi'] position_manager_address = Address( "0xC36442b4a4522E871399CD717aBDD847Ab11FE88") swap_router_address = Address("0xE592427A0AEce92De3Edee1F18E0157C05861564") quoter_address = Address("0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6") mainnet_weth_address = Address("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") factory_address = Address("0x1F98431c8aD98523631AE4a59f267346ea31F984") ticklens_address = Address("0xbfd8137f7d1516D3ea5cA83523914859ec47F573") DAI_KOVAN_ADDRESS = Address("0x4f96fe3b7a6cf9725f59d353f723c1bdb64ca6aa") DAI_MAINNET_ADDRESS = Address("0x6B175474E89094C44Da98b954EedeAC495271d0F") WETH_KOVAN_ADDRESS = Address("0xd0a1e359811322d97991e03f863a0c30c2cf029c") USDC_KOVAN_ADDRESS = Address("0x198419c5c340e8de47ce4c0e4711a03664d42cb2") weth_token_kovan = Token("WETH", WETH_KOVAN_ADDRESS, 18)
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. """ abi = Contract._load_abi(__name__, 'abi/EtherDelta.abi') bin = Contract._load_bin(__name__, 'abi/EtherDelta.bin') ETH_TOKEN = Address('0x0000000000000000000000000000000000000000') @staticmethod def deploy(web3: Web3, admin: Address, fee_account: Address, account_levels_addr: Address, fee_make: Wad, fee_take: Wad, fee_rebate: Wad): """Deploy a new instance of the `EtherDelta` contract. Args: web3: An instance of `Web` from `web3.py`. Returns: A `EtherDelta` class instance. """ return EtherDelta( web3=web3, address=Contract._deploy(web3, EtherDelta.abi, EtherDelta.bin, [ admin.address, fee_account.address, account_levels_addr.address, fee_make.value, fee_take.value, fee_rebate.value ])) 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) def approve(self, tokens: List[ERC20Token], approval_function): """Approve the EtherDelta 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, 'EtherDelta') def admin(self) -> Address: """Returns the address of the admin account. Returns: The address of the admin account. """ return Address(self._contract.call().admin()) def fee_account(self) -> Address: """Returns the address of the fee account i.e. the account that receives all fees collected. Returns: The address of the fee account. """ return Address(self._contract.call().feeAccount()) def account_levels_addr(self) -> Address: """Returns the address of the AccountLevels contract. Returns: The address of the AccountLevels contract. """ return Address(self._contract.call().accountLevelsAddr()) def fee_make(self) -> Wad: """Returns the maker fee configured in the contract. Returns: The maker fee. """ return Wad(self._contract.call().feeMake()) def fee_take(self) -> Wad: """Returns the taker fee configured in the contract. Returns: The taker fee. """ return Wad(self._contract.call().feeTake()) def fee_rebate(self) -> Wad: """Returns the rebate fee configured in the contract. Plase see the contract source code for more details. Returns: The rebate fee. """ return Wad(self._contract.call().feeRebate()) def on_trade(self, handler, event_filter: dict = None): """Subscribe to LogTrade events. `LogTrade` events are emitted by the EtherDelta contract every time someone takes an order. Args: handler: Function which will be called for each subsequent `LogTrade` event. This handler will receive a :py:class:`pymaker.etherdelta.LogTrade` class instance. event_filter: Filter which will be applied to event subscription. """ assert (callable(handler)) assert (isinstance(event_filter, dict) or (event_filter is None)) self._on_event(self._contract, 'Trade', LogTrade, handler, event_filter) def past_trade(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogTrade]: """Synchronously retrieve past LogTrade events. `LogTrade` events are emitted by the EtherDelta 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 `LogTrade` events represented as :py:class:`pymaker.etherdelta.LogTrade` class. """ assert (isinstance(number_of_past_blocks, int)) assert (isinstance(event_filter, dict) or (event_filter is None)) return self._past_events(self._contract, 'Trade', LogTrade, number_of_past_blocks, event_filter) def deposit(self, amount: Wad) -> Transact: """Deposits `amount` of raw ETH to EtherDelta. Args: amount: Amount of raw ETH to be deposited on EtherDelta. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(amount, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'deposit', [], {'value': amount.value}) def withdraw(self, amount: Wad) -> Transact: """Withdraws `amount` of raw ETH from EtherDelta. The withdrawn ETH will get transferred to the calling account. Args: amount: Amount of raw ETH to be withdrawn from EtherDelta. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(amount, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'withdraw', [amount.value]) def balance_of(self, user: Address) -> Wad: """Returns the amount of raw ETH deposited by the specified user. Args: user: Address of the user to check the balance of. Returns: The raw ETH balance kept in the EtherDelta contract by the specified user. """ assert (isinstance(user, Address)) return Wad(self._contract.call().balanceOf( '0x0000000000000000000000000000000000000000', user.address)) def deposit_token(self, token: Address, amount: Wad) -> Transact: """Deposits `amount` of ERC20 token `token` to EtherDelta. Tokens will be pulled from the calling account, so the EtherDelta contract needs to have appropriate allowance. Either call `approve()` or set the allowance manually before trying to deposit tokens. Args: token: Address of the ERC20 token to be deposited. amount: Amount of token `token` to be deposited to EtherDelta. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(token, Address)) assert (isinstance(amount, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'depositToken', [token.address, amount.value]) def withdraw_token(self, token: Address, amount: Wad) -> Transact: """Withdraws `amount` of ERC20 token `token` from EtherDelta. Tokens will get transferred to the calling account. Args: token: Address of the ERC20 token to be withdrawn. amount: Amount of token `token` to be withdrawn from EtherDelta. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(token, Address)) assert (isinstance(amount, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'withdrawToken', [token.address, amount.value]) def balance_of_token(self, token: Address, user: Address) -> Wad: """Returns the amount of ERC20 token `token` deposited by the specified user. Args: token: Address of the ERC20 token return the balance of. user: Address of the user to check the balance of. Returns: The ERC20 token `token` balance kept in the EtherDelta contract by the specified user. """ assert (isinstance(token, Address)) assert (isinstance(user, Address)) return Wad(self._contract.call().balanceOf(token.address, user.address)) def create_order(self, pay_token: Address, pay_amount: Wad, buy_token: Address, buy_amount: Wad, expires: int) -> Order: """Creates a new off-chain order. Although it's not necessary to have any amount of `pay_token` deposited to EtherDelta before placing an order, nobody will be able to take this order until some balance of 'pay_token' is provided. If you want to trade raw ETH, pass `Address('0x0000000000000000000000000000000000000000')` as either `pay_token` or `buy_token`. 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. expires: The block number after which the order will expire. Returns: Newly created order as an instance of the :py:class:`pymaker.etherdelta.Order` class. """ assert (isinstance(pay_token, Address)) assert (isinstance(pay_amount, Wad)) assert (isinstance(buy_token, Address)) assert (isinstance(buy_amount, Wad)) assert (isinstance(expires, int) and (expires > 0)) assert (pay_amount > Wad(0)) assert (buy_amount > Wad(0)) nonce = self.random_nonce() order_hash = hashlib.sha256( encode_address(self.address) + encode_address(buy_token) + encode_uint256(buy_amount.value) + encode_address(pay_token) + encode_uint256(pay_amount.value) + encode_uint256(expires) + encode_uint256(nonce)).digest() signature = eth_sign(order_hash, self.web3) v, r, s = to_vrs(signature) return Order(self, Address(self.web3.eth.defaultAccount), pay_token, pay_amount, buy_token, buy_amount, expires, nonce, v, r, s) 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.buy_amount - 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. Returns: The available amount for the order, in terms of `buy_token`. """ assert (isinstance(order, Order)) return Wad(self._contract.call().availableVolume( order.buy_token.address, order.buy_amount.value, order.pay_token.address, order.pay_amount.value, order.expires, order.nonce, order.maker.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.buy_amount`. 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.buy_amount`. Cancelled orders basically look like completely filled ones. Args: order: The order object you want to know the filled amount of. Returns: The amount already filled for the order, in terms of `buy_token`. """ assert (isinstance(order, Order)) return Wad(self._contract.call().amountFilled( order.buy_token.address, order.buy_amount.value, order.pay_token.address, order.pay_amount.value, order.expires, order.nonce, order.maker.address, order.v if hasattr(order, 'v') else 0, order.r if hasattr(order, 'r') else bytes(), order.s if hasattr(order, 's') else bytes())) def trade(self, order: Order, amount: Wad) -> Transact: """Takes (buys) an order. `amount` is in `buy_token` terms, it is the amount you want to buy with. It can not be higher than `amount_available(order)`. The 'amount' of `buy_token` tokens will get deducted from your EtherDelta balance if the trade was successful. The corresponding amount of `pay_token` tokens will be added to your EtherDelta balance. Args: order: The order you want to take (buy). amount: Amount of `buy_token` tokens that you want to be deducted from your EtherDelta balance in order to buy a corresponding amount of `pay_token` tokens. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(order, Order)) assert (isinstance(amount, Wad)) return Transact( self, self.web3, self.abi, self.address, self._contract, 'trade', [ order.buy_token.address, order.buy_amount.value, order.pay_token.address, order.pay_amount.value, order.expires, order.nonce, order.maker.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. amount: Amount expressed in terms of `buy_token` 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.buy_token.address, order.buy_amount.value, order.pay_token.address, order.pay_amount.value, order.expires, order.nonce, order.maker.address, order.v if hasattr(order, 'v') else 0, order.r if hasattr(order, 'r') else bytes(), order.s if hasattr(order, 's') else bytes(), amount.value, self.web3.eth.defaultAccount) def cancel_order(self, order: Order) -> Transact: """Cancels an existing order. Orders can be cancelled only by their owners. Args: order: The order you want to cancel. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(order, Order)) return Transact( self, self.web3, self.abi, self.address, self._contract, 'cancelOrder', [ order.buy_token.address, order.buy_amount.value, order.pay_token.address, order.pay_amount.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) def __repr__(self): return f"EtherDelta('{self.address}')"
class DSValue(Contract): """A client for the `DSValue` contract, a single-value data feed. `DSValue` is a single-value data feed, which means it can be in one of two states. It can either contain a value (in which case `has_value()` returns `True` and the read methods return that value) or be empty (in which case `has_value()` returns `False` and the read methods throw exceptions). `DSValue` can be populated with a new value using `poke()` and cleared using `void()`. Everybody can read from a `DSValue`. Calling `poke()` and `void()` is usually whitelisted to some addresses only. The `DSValue` contract keeps the value as a 32-byte array (Ethereum `bytes32` type). Methods have been provided to cast it into `int`, read as hex etc. You can find the source code of the `DSValue` contract here: <https://github.com/dapphub/ds-value>. Attributes: web3: An instance of `Web` from `web3.py`. address: Ethereum address of the `DSValue` contract. """ abi = Contract._load_abi(__name__, 'abi/DSValue.abi') bin = Contract._load_bin(__name__, 'abi/DSValue.bin') @staticmethod def deploy(web3: Web3): return DSValue(web3=web3, address=Contract._deploy(web3, DSValue.abi, DSValue.bin, [])) def __init__(self, web3: Web3, address: Address): assert (isinstance(web3, Web3)) assert (isinstance(address, Address)) self.web3 = web3 self.address = address self._contract = self._get_contract(web3, self.abi, 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. """ return self._contract.call().read() def read_as_hex(self) -> str: """Reads the current value from this instance and converts it to a hex string. If this instance does not contain a value, throws an exception. Returns: A string with a hexadecimal representation of the current value of this instance. """ return ''.join(hex(x)[2:].zfill(2) for x in self.read()) def read_as_int(self) -> int: """Reads the current value from this instance and converts it to an int. If the value is actually a `Ray` or a `Wad`, you can convert it to one using `Ray(...)` or `Wad(...)`. Please see `Ray` or `Wad` for more details. If this instance does not contain a value, throws an exception. Returns: An integer representation of the current value of this instance. """ return int(self.read_as_hex(), 16) def poke(self, new_value: bytes) -> Transact: """Populates this instance with a new value. Args: new_value: A 32-byte array with the new value to be set. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(new_value, bytes)) assert (len(new_value) == 32) return Transact(self, self.web3, self.abi, self.address, self._contract, 'poke', [new_value]) def poke_with_int(self, new_value: int) -> Transact: """Populates this instance with a new value. Handles the conversion of a Python `int` into the Solidity `bytes32` type automatically. If the value you want to set is actually a `Ray` or a `Wad`, you can get the integer value from them by accessing their `value` property. Please see `Ray` or `Wad` for more details. Args: new_value: A non-negative integer with the new value to be set. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(new_value, int)) assert (new_value >= 0) return self.poke(new_value.to_bytes(32, byteorder='big')) def void(self) -> Transact: """Removes the current value from this instance. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ return Transact(self, self.web3, self.abi, self.address, self._contract, 'void', []) def __repr__(self): return f"DSValue('{self.address}')"
class ZrxExchangeV2(Contract): """A client for the 0x V2 exchange contract. You can find the `0x V2` exchange contract here: <https://etherscan.io/address/0x4f833a24e1f95d70f028921e27040ca56e09ab0b>. Attributes: web3: An instance of `Web` from `web3.py`. address: Ethereum address of the _0x_ `Exchange` contract. """ abi = Contract._load_abi(__name__, 'abi/ExchangeV2.abi') bin = Contract._load_bin(__name__, 'abi/ExchangeV2.bin') _ZERO_ADDRESS = Address("0x0000000000000000000000000000000000000000") ORDER_INFO_TYPE = '(address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)' @staticmethod def deploy(web3: Web3, zrx_asset: str): """Deploy a new instance of the 0x `Exchange` contract. Args: web3: An instance of `Web` from `web3.py`. zrx_token: The address of the ZRX token this exchange will use. Returns: A `ZrxExchange` class instance. """ return ZrxExchangeV2(web3=web3, address=Contract._deploy(web3, ZrxExchangeV2.abi, ZrxExchangeV2.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) def zrx_asset(self) -> str: """Get the asset data of the ZRX token contract associated with this `ExchangeV2` contract. Returns: The asset data of the `ZRX` token. """ return str(bytes_to_hexstring(self._contract.call().ZRX_ASSET_DATA())) def zrx_token(self) -> Address: """Get the address of the ZRX token contract associated with this `ExchangeV2` contract. Returns: The address of the `ZRX` token. """ return Address("0x" + self.zrx_asset()[-40:]) def asset_transfer_proxy(self, proxy_id: str) -> Address: """Get the address of the `ERC20Proxy` contract associated with this `Exchange` contract. Returns: The address of the `ERC20Proxy` token. """ assert (isinstance(proxy_id, str)) return Address(self._contract.call().getAssetProxy( hexstring_to_bytes(proxy_id))) def approve(self, tokens: List[ERC20Token], approval_function): """Approve the 0x ERC20Proxy contract to fully access balances of specified tokens. In case of 0x V2, it's the ERC20Proxy contract that actually gets the approvals, not the 0x Exchange contract itself. In addition to the tokens specified as the `tokens` parameter, the ZRX token always gets approved as well as without it the 0x Exchange contract wouldn't be able to charge maker and taker fees. 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: # TODO + [ERC20Token(web3=self.web3, address=self.zrx_token())] approval_function(token, self.asset_transfer_proxy(ERC20Asset.ID), '0x ERC20Proxy contract') def past_fill(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogFill]: """Synchronously retrieve past LogFill events. `LogFill` events are emitted by the 0x contract every time someone fills 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 `LogFill` events represented as :py:class:`pymaker.zrx.LogFill` class. """ assert (isinstance(number_of_past_blocks, int)) assert (isinstance(event_filter, dict) or (event_filter is None)) return self._past_events(self._contract, 'Fill', LogFill, number_of_past_blocks, event_filter) def past_cancel(self, number_of_past_blocks: int, event_filter: dict = None) -> List[LogCancel]: """Synchronously retrieve past LogCancel events. `LogCancel` events are emitted by the 0x 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 `LogCancel` events represented as :py:class:`pymaker.zrx.LogCancel` class. """ assert (isinstance(number_of_past_blocks, int)) assert (isinstance(event_filter, dict) or (event_filter is None)) return self._past_events(self._contract, 'Cancel', LogCancel, number_of_past_blocks, event_filter) def create_order(self, pay_asset: Asset, pay_amount: Wad, buy_asset: Asset, buy_amount: Wad, expiration: int) -> Order: """Creates a new order. The `maker_fee`, `taker_fee` and `fee_recipient` fields are by default set to zero. Before signing the order and submitting it to the relayer, they may need to be populated using the `calculate_fees()` method of the `ZrxRelayerApi` class. Args: pay_asset: The asset you want to put on sale. pay_amount: Amount of the `pay_asset` token you want to put on sale. buy_asset: The asset you want to be paid with. buy_amount: Amount of the `buy_asset` you want to receive. expiration: Unix timestamp (in seconds) when the order will expire. Returns: New order as an instance of the :py:class:`pymaker.zrx.Order` class. """ assert (isinstance(pay_asset, Asset)) assert (isinstance(pay_amount, Wad)) assert (isinstance(buy_asset, Asset)) assert (isinstance(buy_amount, Wad)) assert (isinstance(expiration, int)) return Order(exchange=self, sender=self._ZERO_ADDRESS, maker=Address(self.web3.eth.defaultAccount), taker=self._ZERO_ADDRESS, maker_fee=Wad(0), taker_fee=Wad(0), pay_asset=pay_asset, pay_amount=pay_amount, buy_asset=buy_asset, buy_amount=buy_amount, salt=self.random_salt(), fee_recipient=self._ZERO_ADDRESS, expiration=expiration, exchange_contract_address=self.address, signature=None) def _get_order_info(self, order): assert (isinstance(order, Order)) method_signature = self.web3.sha3( text=f"getOrderInfo({self.ORDER_INFO_TYPE})")[0:4] method_parameters = encode_single(f"({self.ORDER_INFO_TYPE})", [self._order_tuple(order)]) request = bytes_to_hexstring(method_signature + method_parameters) response = self.web3.eth.call({ 'to': self.address.address, 'data': request }) response_decoded = decode_single("((uint8,bytes32,uint256))", response) return response_decoded def get_order_hash(self, order: Order) -> str: """Calculates hash of an order. Args: order: Order you want to calculate the hash of. Returns: Order hash as a hex string starting with `0x`. """ assert (isinstance(order, Order)) # the hash depends on the exchange contract address as well assert (order.exchange_contract_address == self.address) return bytes_to_hexstring(self._get_order_info(order)[0][1]) def get_unavailable_buy_amount(self, order: Order) -> Wad: """Return the order amount which was either taken or cancelled. Args: order: Order you want to get the unavailable amount of. Returns: The unavailable amount of the order (i.e. the amount which was either taken or cancelled), expressed in terms of the `buy_token` token. """ assert (isinstance(order, Order)) order_info = self._get_order_info(order)[0] if order_info[0] in [ 0, # INVALID, // Default value 1, # INVALID_MAKER_ASSET_AMOUNT, // Order does not have a valid maker asset amount 2, # INVALID_TAKER_ASSET_AMOUNT, // Order does not have a valid taker asset amount 4, # EXPIRED, // Order has already expired 5, # FULLY_FILLED, // Order is fully filled 6 ]: # CANCELLED // Order has been cancelled return order.buy_amount else: return Wad(order_info[2]) def sign_order(self, order: Order) -> Order: """Signs an order so it can be submitted to the relayer. Order will be signed by the `web3.eth.defaultAccount` account. Args: order: Order you want to sign. Returns: Signed order. Copy of the order passed as a parameter with the `signature` field filled with signature. """ assert (isinstance(order, Order)) signature = eth_sign(hexstring_to_bytes(self.get_order_hash(order)), self.web3) v, r, s = to_vrs(signature) signed_order = copy.copy(order) signed_order.signature = bytes_to_hexstring(bytes([v])) + \ bytes_to_hexstring(r)[2:] + \ bytes_to_hexstring(s)[2:] + \ "03" # EthSign return signed_order def fill_order(self, order: Order, fill_buy_amount: Wad) -> Transact: """Fills an order. Args: order: The order to be filled. fill_buy_amount: The amount (in terms of `buy_token` of the original order) to be filled. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(order, Order)) assert (isinstance(fill_buy_amount, Wad)) method_signature = self.web3.sha3( text=f"fillOrder({self.ORDER_INFO_TYPE},uint256,bytes)")[0:4] method_parameters = encode_single( f"({self.ORDER_INFO_TYPE},uint256,bytes)", [ self._order_tuple(order), fill_buy_amount.value, hexstring_to_bytes(order.signature) ]) request = bytes_to_hexstring(method_signature + method_parameters) return Transact(self, self.web3, self.abi, self.address, self._contract, None, [request]) def cancel_order(self, order: Order) -> Transact: """Cancels an order. Args: order: Order you want to cancel. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(order, Order)) method_signature = self.web3.sha3( text=f"cancelOrder({self.ORDER_INFO_TYPE})")[0:4] method_parameters = encode_single(f"({self.ORDER_INFO_TYPE})", [self._order_tuple(order)]) request = bytes_to_hexstring(method_signature + method_parameters) return Transact(self, self.web3, self.abi, self.address, self._contract, None, [request]) @staticmethod def _order_tuple(order): return (order.maker.address, order.taker.address, order.fee_recipient.address, order.sender.address, order.pay_amount.value, order.buy_amount.value, order.maker_fee.value, order.taker_fee.value, order.expiration, order.salt, hexstring_to_bytes(order.pay_asset.serialize()), hexstring_to_bytes(order.buy_asset.serialize())) @staticmethod def random_salt() -> int: return int(time.time() * 1000) def __repr__(self): return f"ZrxExchangeV2('{self.address}')"
class Flopper(AuctionContract): """A client for the `Flopper` contract, used to interact with debt auctions. You can find the source code of the `Flopper` contract here: <https://github.com/makerdao/dss/blob/master/src/flop.sol>. Attributes: web3: An instance of `Web` from `web3.py`. address: Ethereum address of the `Flopper` contract. Event signatures: 0x65fae35e: (deployment-related) 0x9c52a7f1: (deployment-related) 0x29ae8114: file 0x7e8881001566f9f89aedb9c5dc3d856a2b81e5235a8196413ed484be91cc0df6: kick 0x5ff3a382: dent 0xc959c42b: deal """ abi = Contract._load_abi(__name__, 'abi/Flopper.abi') bin = Contract._load_bin(__name__, 'abi/Flopper.bin') class Bid: def __init__(self, id: int, bid: Rad, lot: Wad, guy: Address, tic: int, end: int): assert(isinstance(id, int)) assert(isinstance(bid, Rad)) assert(isinstance(lot, Wad)) assert(isinstance(guy, Address)) assert(isinstance(tic, int)) assert(isinstance(end, int)) self.id = id self.bid = bid self.lot = lot self.guy = guy self.tic = tic self.end = end def __repr__(self): return f"Flopper.Bid({pformat(vars(self))})" class KickLog: def __init__(self, log): args = log['args'] self.id = args['id'] self.lot = Wad(args['lot']) self.bid = Rad(args['bid']) self.gal = Address(args['gal']) self.block = log['blockNumber'] self.tx_hash = log['transactionHash'].hex() def __repr__(self): return f"Flopper.KickLog({pformat(vars(self))})" class DentLog: def __init__(self, lognote: LogNote): self.guy = Address(lognote.usr) self.id = Web3.toInt(lognote.arg1) self.lot = Wad(Web3.toInt(lognote.arg2)) self.bid = Rad(Web3.toInt(lognote.get_bytes_at_index(2))) self.block = lognote.block self.tx_hash = lognote.tx_hash def __repr__(self): return f"Flopper.DentLog({pformat(vars(self))})" def __init__(self, web3: Web3, address: Address): assert isinstance(web3, Web3) assert isinstance(address, Address) super(Flopper, self).__init__(web3, address, Flopper.abi, self.bids) def live(self) -> bool: return self._contract.functions.live().call() > 0 def pad(self) -> Wad: """Returns the lot increase applied after an auction has been `tick`ed.""" return Wad(self._contract.functions.pad().call()) def bids(self, id: int) -> Bid: """Returns the auction details. Args: id: Auction identifier. Returns: The auction details. """ assert(isinstance(id, int)) array = self._contract.functions.bids(id).call() return Flopper.Bid(id=id, bid=Rad(array[0]), lot=Wad(array[1]), guy=Address(array[2]), tic=int(array[3]), end=int(array[4])) def kick(self, gal: Address, lot: Wad, bid: Wad) -> Transact: assert(isinstance(gal, Address)) assert(isinstance(lot, Wad)) assert(isinstance(bid, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'kick', [gal.address, lot.value, bid.value]) def dent(self, id: int, lot: Wad, bid: Rad) -> Transact: assert(isinstance(id, int)) assert(isinstance(lot, Wad)) assert(isinstance(bid, Rad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'dent', [id, lot.value, bid.value]) def yank(self, id: int) -> Transact: """While `cage`d, refund current bid to the bidder""" assert (isinstance(id, int)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'yank', [id]) def past_logs(self, from_block: int, to_block: int = None): logs = super().get_past_lognotes(Flopper.abi, from_block, to_block) history = [] for log in logs: if log is None: continue elif isinstance(log, Flopper.KickLog): history.append(log) elif log.sig == '0x5ff3a382': history.append(Flopper.DentLog(log)) elif log.sig == '0xc959c42b': history.append(AuctionContract.DealLog(log)) return history def parse_event(self, event): signature = Web3.toHex(event['topics'][0]) codec = ABICodec(default_registry) if signature == "0x7e8881001566f9f89aedb9c5dc3d856a2b81e5235a8196413ed484be91cc0df6": event_data = get_event_data(codec, self.kick_abi, event) return Flopper.KickLog(event_data) else: event_data = get_event_data(codec, self.log_note_abi, event) return LogNote(event_data) def __repr__(self): return f"Flopper('{self.address}')"
class Flapper(AuctionContract): """A client for the `Flapper` contract, used to interact with surplus auctions. You can find the source code of the `Flapper` contract here: <https://github.com/makerdao/dss/blob/master/src/flap.sol>. Attributes: web3: An instance of `Web` from `web3.py`. address: Ethereum address of the `Flapper` contract. Event signatures: 0x65fae35e: (deployment-related) 0x9c52a7f1: (deployment-related) 0xe6dde59cbc017becba89714a037778d234a84ce7f0a137487142a007e580d609: kick 0x29ae8114: file 0x4b43ed12: tend 0xc959c42b: deal """ abi = Contract._load_abi(__name__, 'abi/Flapper.abi') bin = Contract._load_bin(__name__, 'abi/Flapper.bin') class Bid: def __init__(self, id: int, bid: Wad, lot: Rad, guy: Address, tic: int, end: int): assert(isinstance(id, int)) assert(isinstance(bid, Wad)) # MKR assert(isinstance(lot, Rad)) # DAI assert(isinstance(guy, Address)) assert(isinstance(tic, int)) assert(isinstance(end, int)) self.id = id self.bid = bid self.lot = lot self.guy = guy self.tic = tic self.end = end def __repr__(self): return f"Flapper.Bid({pformat(vars(self))})" class KickLog: def __init__(self, log): args = log['args'] self.id = args['id'] self.lot = Rad(args['lot']) self.bid = Wad(args['bid']) self.block = log['blockNumber'] self.tx_hash = log['transactionHash'].hex() def __repr__(self): return f"Flapper.KickLog({pformat(vars(self))})" class TendLog: def __init__(self, lognote: LogNote): self.guy = Address(lognote.usr) self.id = Web3.toInt(lognote.arg1) self.lot = Rad(Web3.toInt(lognote.arg2)) self.bid = Wad(Web3.toInt(lognote.get_bytes_at_index(2))) self.block = lognote.block self.tx_hash = lognote.tx_hash def __repr__(self): return f"Flapper.TendLog({pformat(vars(self))})" def __init__(self, web3: Web3, address: Address): super(Flapper, self).__init__(web3, address, Flapper.abi, self.bids) def live(self) -> bool: return self._contract.functions.live().call() > 0 def bids(self, id: int) -> Bid: """Returns the auction details. Args: id: Auction identifier. Returns: The auction details. """ assert(isinstance(id, int)) array = self._contract.functions.bids(id).call() return Flapper.Bid(id=id, bid=Wad(array[0]), lot=Rad(array[1]), guy=Address(array[2]), tic=int(array[3]), end=int(array[4])) def kick(self, lot: Rad, bid: Wad) -> Transact: assert(isinstance(lot, Rad)) assert(isinstance(bid, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'kick', [lot.value, bid.value]) def tend(self, id: int, lot: Rad, bid: Wad) -> Transact: assert(isinstance(id, int)) assert(isinstance(lot, Rad)) assert(isinstance(bid, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'tend', [id, lot.value, bid.value]) def yank(self, id: int) -> Transact: """While `cage`d, refund current bid to the bidder""" assert (isinstance(id, int)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'yank', [id]) def past_logs(self, from_block: int, to_block: int = None): logs = super().get_past_lognotes(Flapper.abi, from_block, to_block) history = [] for log in logs: if log is None: continue elif isinstance(log, Flapper.KickLog): history.append(log) elif log.sig == '0x4b43ed12': history.append(Flapper.TendLog(log)) elif log.sig == '0xc959c42b': history.append(AuctionContract.DealLog(log)) return history def parse_event(self, event): signature = Web3.toHex(event['topics'][0]) codec = ABICodec(default_registry) if signature == "0xe6dde59cbc017becba89714a037778d234a84ce7f0a137487142a007e580d609": event_data = get_event_data(codec, self.kick_abi, event) return Flapper.KickLog(event_data) else: event_data = get_event_data(codec, self.log_note_abi, event) return LogNote(event_data) def __repr__(self): return f"Flapper('{self.address}')"
def deploy(web3: Web3): return TxManager(web3=web3, address=Contract._deploy(web3, TxManager.abi, TxManager.bin, []))
class DSToken(ERC20Token): """A client for the `DSToken` contract. You can find the source code of the `DSToken` contract here: <https://github.com/dapphub/ds-token>. Attributes: web3: An instance of `Web` from `web3.py`. address: Ethereum address of the `DSToken` contract. """ abi = Contract._load_abi(__name__, 'abi/DSToken.abi') bin = Contract._load_bin(__name__, 'abi/DSToken.bin') @staticmethod def deploy(web3: Web3, symbol: str): """Deploy a new instance of the `DSToken` contract. Args: web3: An instance of `Web` from `web3.py`. symbol: Symbol of the new token. Returns: A `DSToken` class instance. """ assert (isinstance(symbol, str)) return DSToken(web3=web3, address=Contract._deploy(web3, DSToken.abi, DSToken.bin, [bytes(symbol, "utf-8")])) def authority(self) -> Address: """Return the current `authority` of a `DSAuth`-ed contract. Returns: The address of the current `authority`. """ return Address(self._contract.functions.authority().call()) def set_authority(self, address: Address) -> Transact: """Set the `authority` of a `DSAuth`-ed contract. Args: address: The address of the new `authority`. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(address, Address)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'setAuthority', [address.address]) def mint(self, amount: Wad) -> Transact: """Increase the total supply of the token. Args: amount: The amount to increase the total supply by. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(amount, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'mint(uint256)', [amount.value]) def mint_to(self, address: Address, amount: Wad) -> Transact: """Increase the total supply of the token. Args: address: The address to credit the new tokens to. amount: The amount to increase the total supply by. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(amount, Wad)) assert (isinstance(address, Address)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'mint(address,uint256)', [address.address, amount.value]) def burn(self, amount: Wad) -> Transact: """Decrease the total supply of the token. Args: amount: The amount to decrease the total supply by. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(amount, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'burn(uint256)', [amount.value]) def burn_from(self, address: Address, amount: Wad) -> Transact: """Decrease the total supply of the token. Args: address: The address to burn the tokens from. amount: The amount to decrease the total supply by. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(amount, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'burn(address,uint256)', [address.address, amount.value]) def __repr__(self): return f"DSToken('{self.address}')"
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. """ abi = Contract._load_abi(__name__, 'abi/SaiTub.abi') bin = Contract._load_bin(__name__, 'abi/SaiTub.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, sai: Address, sin: Address, skr: Address, gem: Address, gov: Address, pip: Address, pep: Address, vox: Address, pit: Address): assert(isinstance(sai, Address)) assert(isinstance(sin, Address)) assert(isinstance(skr, Address)) assert(isinstance(gem, Address)) assert(isinstance(gov, Address)) assert(isinstance(pip, Address)) assert(isinstance(pep, Address)) assert(isinstance(vox, Address)) assert(isinstance(pit, Address)) return Tub(web3=web3, address=Contract._deploy(web3, Tub.abi, Tub.bin, [sai.address, sin.address, skr.address, gem.address, gov.address, pip.address, pep.address, vox.address, pit.address])) def set_authority(self, address: Address) -> Transact: assert(isinstance(address, Address)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'setAuthority', [address.address]) def approve(self, approval_function): """Approve the `Tub` to access our GEM, SKR, SAI and GOV balances. For available approval functions (i.e. approval modes) see `directly` and `via_tx_manager` in `pymaker.approval`. Args: approval_function: Approval function (i.e. approval mode). """ assert(callable(approval_function)) approval_function(ERC20Token(web3=self.web3, address=self.gem()), self.address, 'Tub') approval_function(ERC20Token(web3=self.web3, address=self.skr()), self.address, 'Tub') approval_function(ERC20Token(web3=self.web3, address=self.sai()), self.address, 'Tub') approval_function(ERC20Token(web3=self.web3, address=self.gov()), self.address, 'Tub') def era(self) -> int: """Return the current `Tub` timestamp. Returns: Timestamp as a unix timestamp. """ return self._contract.call().era() def tap(self) -> Address: """Get the address of the `Tap` contract. Returns: The address of the `Tap` contract. """ return Address(self._contract.call().tap()) def sai(self) -> Address: """Get the SAI token. Returns: The address of the SAI token. """ return Address(self._contract.call().sai()) def sin(self) -> Address: """Get the SIN token. Returns: The address of the SIN token. """ return Address(self._contract.call().sin()) def gov(self) -> Address: """Get the MKR token. Returns: The address of the MKR token. """ return Address(self._contract.call().gov()) def vox(self) -> Address: """Get the address of the `Vox` contract. Returns: The address of the `Vox` contract. """ return Address(self._contract.call().vox()) def pit(self) -> Address: """Get the governance vault. Returns: The address of the `DSVault` holding the governance tokens awaiting burn. """ return Address(self._contract.call().pit()) def skr(self) -> Address: """Get the SKR token. Returns: The address of the SKR token. """ return Address(self._contract.call().skr()) def gem(self) -> Address: """Get the collateral token (eg. W-ETH). Returns: The address of the collateral token. """ return Address(self._contract.call().gem()) def pip(self) -> Address: """Get the reference (GEM) price feed. Returns: The address of the reference (GEM) price feed, which could be a `DSValue`, a `DSCache`, `Mednianizer` etc. """ return Address(self._contract.call().pip()) def pep(self) -> Address: """Get the governance (MKR) price feed. Returns: The address of the governance (MKR) price feed, which could be a `DSValue`, a `DSCache`, `Mednianizer` etc. """ return Address(self._contract.call().pep()) 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._contract.call().axe()) def cap(self) -> Wad: """Get the debt ceiling. Returns: The debt ceiling in SAI. """ return Wad(self._contract.call().cap()) def mat(self) -> Ray: """Get the liquidation ratio. Returns: The liquidation ratio. `1.5` means the liquidation ratio is 150%. """ return Ray(self._contract.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._contract.call().tax()) def reg(self) -> int: """Get the Tub stage ('register'). Returns: The current Tub stage (0=Usual, 1=Caged). """ return self._contract.call().reg() def fit(self) -> Ray: """Get the GEM per SKR settlement price. Returns: The GEM per SKR settlement (kill) price. """ return Ray(self._contract.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._contract.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._contract.call().chi()) def mold_axe(self, new_axe: Ray) -> Transact: """Update the liquidation penalty. Args: new_axe: The new value of the liquidation penalty (`axe`). `1.0` means no penalty. `1.2` means 20% penalty. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert isinstance(new_axe, Ray) return Transact(self, self.web3, self.abi, self.address, self._contract, 'mold', ['axe', new_axe.value]) def mold_cap(self, new_cap: Wad) -> Transact: """Update the debt ceiling. Args: new_cap: The new value of the debt ceiling (`cap`), in SAI. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert isinstance(new_cap, Wad) return Transact(self, self.web3, self.abi, self.address, self._contract, 'mold', ['cap', new_cap.value]) def mold_mat(self, new_mat: Ray) -> Transact: """Update the liquidation ratio. Args: new_mat: The new value of the liquidation ratio (`mat`). `1.5` means the liquidation ratio is 150%. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert isinstance(new_mat, Ray) return Transact(self, self.web3, self.abi, self.address, self._contract, 'mold', ['mat', new_mat.value]) def mold_tax(self, new_tax: Ray) -> Transact: """Update the stability fee. Args: new_tax: The new per-second value of the stability fee (`tax`). `1.0` means no stability fee. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert isinstance(new_tax, Ray) return Transact(self, self.web3, self.abi, self.address, self._contract, 'mold', ['tax', new_tax.value]) def mold_gap(self, new_gap: Wad) -> Transact: """Update the current spread (`gap`) for `join` and `exit`. Args: new_tax: The new value of the spread (`gap`). `1.0` means no spread, `1.01` means 1% spread. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert isinstance(new_gap, Wad) return Transact(self, self.web3, self.abi, self.address, self._contract, 'mold', ['gap', new_gap.value]) def drip(self) -> Transact: """Recalculate the internal debt price (`chi`). Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ return Transact(self, self.web3, self.abi, self.address, self._contract, 'drip', []) def prod(self) -> Transact: """Recalculate the accrued holder fee (`par`). Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ return Transact(self, self.web3, self.abiTip, self.tip(), self._contractTip, 'prod', []) def din(self) -> Wad: """Get the amount of total debt. Returns: The amount of total debt in SAI. """ return Wad(self._contract.call().din()) def pie(self) -> Wad: """Get the amount of raw collateral. Returns: The amount of raw collateral in GEM. """ return Wad(self._contract.call().pie()) def air(self) -> Wad: """Get the amount of backing collateral. Returns: The amount of backing collateral in SKR. """ return Wad(self._contract.call().air()) def tag(self) -> Ray: """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 Ray(self._contract.call().tag()) 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 `ask()` and `bid()` respectively. The difference is due to the spread (`gap`). Returns: The current GEM per SKR price. """ return Ray(self._contract.call().per()) def 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._contract.call().gap()) def bid(self, amount: Wad) -> Wad: """Get the current `exit()`. Returns: The amount of GEM you will get for `amount` SKR in `join()`. """ assert(isinstance(amount, Wad)) return Wad(self._contract.call().bid(amount.value)) def ask(self, amount: Wad) -> Wad: """Get the current `join()` price. Returns: The amount of GEM you will have to pay to get `amount` SKR fromm `join()`. """ assert(isinstance(amount, Wad)) return Wad(self._contract.call().ask(amount.value)) 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._contract.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._contract.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._contract.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._contract.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._contract.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._contract.call().safe(int_to_bytes32(cup_id)) def join(self, amount_in_skr: Wad) -> Transact: """Buy SKR for GEMs. Args: amount_in_skr: The amount of SKRs to buy for GEM. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert isinstance(amount_in_skr, Wad) return Transact(self, self.web3, self.abi, self.address, self._contract, 'join', [amount_in_skr.value]) def exit(self, amount_in_skr: Wad) -> Transact: """Sell SKR for GEMs. Args: amount_in_skr: The amount of SKR to sell for GEMs. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert isinstance(amount_in_skr, Wad) return Transact(self, self.web3, self.abi, self.address, self._contract, 'exit', [amount_in_skr.value]) #TODO make it return the id of the newly created cup def open(self) -> Transact: """Create a new cup. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ return Transact(self, self.web3, self.abi, self.address, self._contract, 'open', []) def shut(self, cup_id: int) -> Transact: """Close a cup. Involves calling `wipe()` and `free()` internally in order to clear all remaining SAI debt and free all remaining SKR collateral. Args: cup_id: Id of the cup to close. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert isinstance(cup_id, int) return Transact(self, self.web3, self.abi, self.address, self._contract, 'shut', [int_to_bytes32(cup_id)]) def lock(self, cup_id: int, amount_in_skr: Wad) -> Transact: """Post additional SKR collateral to a cup. Args: cup_id: Id of the cup to post the collateral into. amount_in_skr: The amount of collateral to post, in SKR. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert isinstance(cup_id, int) assert isinstance(amount_in_skr, Wad) return Transact(self, self.web3, self.abi, self.address, self._contract, 'lock', [int_to_bytes32(cup_id), amount_in_skr.value]) def free(self, cup_id: int, amount_in_skr: Wad) -> Transact: """Remove excess SKR collateral from a cup. Args: cup_id: Id of the cup to remove the collateral from. amount_in_skr: The amount of collateral to remove, in SKR. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert isinstance(cup_id, int) assert isinstance(amount_in_skr, Wad) return Transact(self, self.web3, self.abi, self.address, self._contract, 'free', [int_to_bytes32(cup_id), amount_in_skr.value]) def draw(self, cup_id: int, amount_in_sai: Wad) -> Transact: """Issue the specified amount of SAI stablecoins. Args: cup_id: Id of the cup to issue the SAI from. amount_in_sai: The amount SAI to be issued. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert isinstance(cup_id, int) assert isinstance(amount_in_sai, Wad) return Transact(self, self.web3, self.abi, self.address, self._contract, 'draw', [int_to_bytes32(cup_id), amount_in_sai.value]) def wipe(self, cup_id: int, amount_in_sai: Wad) -> Transact: """Repay some portion of existing SAI debt. Args: cup_id: Id of the cup to repay the SAI to. amount_in_sai: The amount SAI to be repaid. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert isinstance(cup_id, int) assert isinstance(amount_in_sai, Wad) return Transact(self, self.web3, self.abi, self.address, self._contract, 'wipe', [int_to_bytes32(cup_id), amount_in_sai.value]) def give(self, cup_id: int, new_lad: Address) -> Transact: """Transfer ownership of a cup. Args: cup_id: Id of the cup to transfer ownership of. new_lad: New owner of the cup. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert isinstance(cup_id, int) assert isinstance(new_lad, Address) return Transact(self, self.web3, self.abi, self.address, self._contract, 'give', [int_to_bytes32(cup_id), new_lad.address]) def bite(self, cup_id: int) -> Transact: """Initiate liquidation of an undercollateralized cup. Args: cup_id: Id of the cup to liquidate. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert isinstance(cup_id, int) return Transact(self, self.web3, self.abi, self.address, self._contract, 'bite', [int_to_bytes32(cup_id)]) def __eq__(self, other): assert(isinstance(other, Tub)) return self.address == other.address def __repr__(self): return f"Tub('{self.address}')"
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: 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) def name(self) -> str: abi_with_string = json.loads( """[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"}]""" ) abi_with_bytes32 = json.loads( """[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"}]""" ) contract_with_string = self._get_contract(self.web3, abi_with_string, self.address) contract_with_bytes32 = self._get_contract(self.web3, abi_with_bytes32, self.address) try: return contract_with_string.name().call() except: return str(contract_with_bytes32.name().call(), "utf-8").strip('\x00') def symbol(self) -> str: abi_with_string = json.loads( """[{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"}]""" ) abi_with_bytes32 = json.loads( """[{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"}]""" ) contract_with_string = self._get_contract(self.web3, abi_with_string, self.address) contract_with_bytes32 = self._get_contract(self.web3, abi_with_bytes32, self.address) try: return contract_with_string.symbol().call() except: return str(contract_with_bytes32.functions.symbol().call(), "utf-8").strip('\x00') def total_supply(self) -> Wad: """Returns the total supply of the token. Returns: The total supply of the token. """ return Wad(self._contract.functions.totalSupply().call()) 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. """ assert (isinstance(address, Address)) return Wad(self._contract.functions.balanceOf(address.address).call()) 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`. """ assert (isinstance(address, Address)) assert (isinstance(payee, Address)) return Wad( self._contract.functions.allowance(address.address, payee.address).call()) def transfer(self, address: Address, value: Wad) -> Transact: """Transfers tokens to a specified address. Args: address: Destination address to transfer the tokens to. value: The value of tokens to transfer. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(address, Address)) assert (isinstance(value, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'transfer', [address.address, value.value]) def transfer_from(self, source_address: Address, destination_address: Address, value: Wad) -> Transact: """Transfers tokens to a specified address. Args: source_address: Source address to transfer the tokens from. destination_address: Destination address to transfer the tokens to. value: The value of tokens to transfer. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(source_address, Address)) assert (isinstance(destination_address, Address)) assert (isinstance(value, Wad)) return Transact( self, self.web3, self.abi, self.address, self._contract, 'transferFrom', [source_address.address, destination_address.address, value.value]) def approve(self, payee: Address, limit: Wad = Wad(2**256 - 1)) -> Transact: """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 :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(payee, Address)) assert (isinstance(limit, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'approve(address,uint256)', [payee.address, limit.value]) def __eq__(self, other): return self.address == other.address def __repr__(self): return f"ERC20Token('{self.address}')"
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]))
class DSEthToken(ERC20Token): """A client for the `DSEthToken` contract. `DSEthToken`, also known as ETH Wrapper or W-ETH, is a contract into which you can deposit raw ETH and then deal with it like with any other ERC20 token. In addition to the `deposit()` and `withdraw()` methods, it implements the standard ERC20 token API. You can find the source code of the `DSEthToken` contract here: <https://github.com/dapphub/ds-eth-token>. Attributes: web3: An instance of `Web` from `web3.py`. address: Ethereum address of the `DSEthToken` contract. """ abi = Contract._load_abi(__name__, 'abi/DSEthToken.abi') bin = Contract._load_bin(__name__, 'abi/DSEthToken.bin') @staticmethod def deploy(web3: Web3): """Deploy a new instance of the `DSEthToken` contract. Args: web3: An instance of `Web` from `web3.py`. Returns: A `DSEthToken` class instance. """ return DSEthToken(web3=web3, address=Contract._deploy(web3, DSEthToken.abi, DSEthToken.bin, [])) def __init__(self, web3, address): super().__init__(web3, address) self._contract = self._get_contract(web3, self.abi, address) def deposit(self, amount: Wad) -> Transact: """Deposits `amount` of raw ETH to `DSEthToken`. Args: amount: Amount of raw ETH to be deposited to `DSEthToken`. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(amount, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'deposit', [], {'value': amount.value}) def withdraw(self, amount: Wad) -> Transact: """Withdraws `amount` of raw ETH from `DSEthToken`. The withdrawn ETH will get transferred to the calling account. Args: amount: Amount of raw ETH to be withdrawn from `DSEthToken`. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(amount, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'withdraw', [amount.value]) def __repr__(self): return f"DSEthToken('{self.address}')"
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.call().last_offer_id() def get_order(self, order_id: int) -> Optional[Order]: """Get order details. Args: order_id: The id of the order to get the details of. 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)) array = self._contract.call().offers(order_id) 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. In addition to that, in case of expiring markets, after the market has expired all orders can be cancelled by anyone. 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}')"
def deploy(web3: Web3, dai: Address, gem: Address): assert(isinstance(dai, Address)) assert(isinstance(gem, Address)) return Flipper(web3=web3, address=Contract._deploy(web3, Flipper.abi, Flipper.bin, [dai.address, gem.address]))
def deploy_staking_rewards(self, liquidity_token_address: Address): self.ds_reward_dai = DSToken.deploy(self.web3, 'REWARD_DAI') self.reward_token = Token("REWARD_DAI", self.ds_dai.address, 18) self.uni_staking_rewards_address = Contract._deploy(self.web3, self.uni_staking_rewards_abi, self.uni_staking_rewards_bin, [self.our_address.address, self.reward_token.address.address, liquidity_token_address.address]) self.uni_staking_rewards = UniswapStakingRewards(self.web3, self.our_address, Address(self.uni_staking_rewards_address), "UniswapStakingRewards")
class UniswapV2Analytics(Contract): """ UniswapV2 Graph Protocol Client Graph Protocol Explorer is available here: https://thegraph.com/explorer/subgraph/graphprotocol/uniswap """ pair_abi = Contract._load_abi(__name__, 'abi/IUniswapV2Pair.abi')['abi'] Irouter_abi = Contract._load_abi(__name__, 'abi/IUniswapV2Router02.abi')['abi'] Ifactory_abi = Contract._load_abi(__name__, 'abi/IUniswapV2Factory.abi')['abi'] def __init__(self, web3: Web3, token_config_path: str, keeper_address: Address, router_address: Address, factory_address: Address, graph_url: str = "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2", start_blocks: dict = {}): assert (isinstance(web3, Web3)) assert (isinstance(token_config_path, str)) assert (isinstance(keeper_address, Address)) assert (isinstance(router_address, Address) or router_address is None) assert (isinstance(factory_address, Address) or factory_address is None) assert (isinstance(graph_url, str)) assert (isinstance(start_blocks, dict)) self.graph_client = GraphClient(graph_url) self.web3 = web3 self.router_address = router_address self.factory_address = factory_address self.account_address = keeper_address self.our_last_pair_hour_block = 0 self.all_last_pair_hour_block = 0 self.start_blocks = start_blocks # assume there's 240 blocks in an hour self.number_of_blocks_to_check = 240 # check to ensure that this isn't a mock instance before attempting to retrieve contracts if router_address is not None and factory_address is not None: self._router_contract = self._get_contract(web3, self.Irouter_abi, self.router_address) self._factory_contract = self._get_contract(web3, self.Ifactory_abi, self.factory_address) self.reloadable_config = ReloadableConfig(token_config_path) self._last_config_dict = None self._last_config = None self.token_config = self.get_token_config().token_config def get_current_block(self) -> int: return int(self.web3.eth.getBlock('latest')['number']) # Return our liquidity token balance def get_current_liquidity(self, pair_address: Address) -> Wad: assert (isinstance(pair_address, Address)) pair_contract = self._get_contract(self.web3, self.pair_abi, pair_address) return Wad(pair_contract.functions.balanceOf(self.account_address.address).call()) # Return the total number of liquidity tokens minted for a given pair def get_total_liquidity(self, pair_address: Address) -> Wad: assert (isinstance(pair_address, Address)) pair_contract = self._get_contract(self.web3, self.pair_abi, pair_address) return Wad(pair_contract.functions.totalSupply().call()) def get_pair_address(self, token_a_address: Address, token_b_address: Address) -> Address: assert (isinstance(token_a_address, Address)) assert (isinstance(token_b_address, Address)) return Address(self._factory_contract.functions.getPair(token_a_address.address, token_b_address.address).call({"from": self.account_address.address})) def get_token_config(self): current_config = self.reloadable_config.get_config() if current_config != self._last_config_dict: self._last_config = TokenConfig(current_config) self._last_config_dict = current_config self.logger.info(f"Successfully parsed configuration") return self._last_config def instantiate_tokens(self, pair: str) -> Tuple[Token, Token]: assert (isinstance(pair, str)) def get_address(value) -> Address: return Address(value['tokenAddress']) if 'tokenAddress' in value else None def get_decimals(value) -> int: return value['tokenDecimals'] if 'tokenDecimals' in value else 18 token_a_name = 'WETH' if pair.split('-')[0] == 'ETH' or pair.split('-')[0] == 'WETH' else pair.split('-')[0] token_b_name = 'WETH' if pair.split('-')[1] == 'ETH' or pair.split('-')[1] == 'WETH' else pair.split('-')[1] token_a = Token(token_a_name, get_address(self.token_config[token_a_name]), get_decimals(self.token_config[token_a_name])) token_b = Token(token_b_name, get_address(self.token_config[token_b_name]), get_decimals(self.token_config[token_b_name])) return token_a, token_b def get_our_burn_txs(self, pair_address: Address) -> List: assert (isinstance(pair_address, Address)) get_our_burn_txs_query = """query ($pair: Bytes!, $to: Bytes!) { burns (where: {pair: $pair, to: $to}) { id to timestamp pair { id token0 { id } token1 { id } } transaction { id blockNumber } } } """ variables = { 'pair': pair_address.address.lower(), 'to': self.account_address.address.lower() } result = self.graph_client.query_request(get_our_burn_txs_query, variables)['burns'] sorted_burns = sorted(result, key=lambda burn: burn['transaction']['blockNumber'], reverse=True) return sorted_burns def get_our_mint_txs(self, pair_address: Address) -> dict: assert (isinstance(pair_address, Address)) get_our_mint_txs_query = """query ($pair: Bytes!, $to: Bytes!) { mints (where: {pair: $pair, to: $to}) { amount0 amount1 id to sender timestamp pair { id token0 { id } token1 { id } } transaction { id blockNumber } liquidity } } """ variables = { 'pair': pair_address.address.lower(), 'to': self.account_address.address.lower() } result = self.graph_client.query_request(get_our_mint_txs_query, variables)['mints'] sorted_mints = sorted(result, key=lambda mint: mint['transaction']['blockNumber'], reverse=True) return sorted_mints def get_block_trade(self, pair_address: Address, block: int) -> List: """ Retrieve pair data for a given block """ assert (isinstance(pair_address, Address)) assert (isinstance(block, int)) get_pair_data_query = """query ($id: Bytes!, $block: Int!) { pairs (block: {number: $block}, where: {id: $id}) { totalSupply token0 { id } token1 { id } token0Price token1Price reserve0 reserve1 id } } """ variables = { 'id': pair_address.address.lower(), 'block': block } result = self.graph_client.query_request(get_pair_data_query, variables)['pairs'] return result def get_trades(self, pair: str, page_number: int = 1) -> List[Trade]: """ It is assumed that our liquidity in a pool will be added or removed all at once. Two stage information retrieval: get list of mint events for our address; sort the mints, and identify the timestamp for the last mit event. If the mint was more than 24 hours ago, return the last 24 hours of data. If the mint was less than 24 hours ago, return all data since the mint event. All mint events are sorted by descending timestamp. When querying, retrieve 24 hours of pair data, with a constructor set periodicity in Blocks. The first new trade retrieved will always travel back one period to create a comparison point for amounts and trade direction. Graph Protocol doesn't currently accept queries using Checksum addresses, so all addresses must be lowercased prior to submission. """ assert (isinstance(pair, str)) assert (isinstance(page_number, int)) trades_list = [] base_token, quote_token = self.instantiate_tokens(pair) pair_address = self.get_pair_address(base_token.address, quote_token.address) mint_events = self.get_our_mint_txs(pair_address) burn_events = self.get_our_burn_txs(pair_address) # use the last retrieved from trade-service as a starting point to avoid duplicate trade syncing if self.start_blocks: if self.start_blocks[pair] > self.our_last_pair_hour_block: self.our_last_pair_hour_block = self.start_blocks[pair] current_block = self.get_current_block() one_day_ago_block = int(current_block - (4 * 60 * 24)) if len(mint_events) == 0: return trades_list last_mint_event = mint_events[0] last_mint_block = int(last_mint_event['transaction']['blockNumber']) if len(burn_events) != 0: last_burn_block = int(burn_events[0]['transaction']['blockNumber']) else: last_burn_block = None our_liquidity_balance = Wad.from_number(last_mint_event['liquidity']) current_liquidity = self.get_current_liquidity(pair_address) mints_in_last_day = list(filter(lambda mint: int(mint['transaction']['blockNumber']) > one_day_ago_block, mint_events)) if current_liquidity != Wad.from_number(0) and len(mints_in_last_day) >= 1: start_block = max(self.our_last_pair_hour_block, int(mints_in_last_day[-1]['transaction']['blockNumber'])) end_block = current_block elif current_liquidity != Wad.from_number(0) and len(mints_in_last_day) == 0: start_block = max(self.our_last_pair_hour_block, one_day_ago_block) end_block = current_block elif current_liquidity == Wad.from_number(0) and len(mints_in_last_day) >= 1: start_block = max(self.our_last_pair_hour_block, int(mints_in_last_day[-1]['transaction']['blockNumber'])) end_block = last_burn_block if last_burn_block != None else current_block else: return trades_list raw_block_trades = [] checked_block = start_block while checked_block + self.number_of_blocks_to_check < end_block: # Query previous time slice to provide a comparison point for reserve amount changes if len(raw_block_trades) == 0: block_trade = self.get_block_trade(pair_address, checked_block - self.number_of_blocks_to_check)[0] raw_block_trades.append(block_trade) block_trade = self.get_block_trade(pair_address, checked_block)[0] raw_block_trades.append(block_trade) # use len of raw_block_trades to access previous trades index = len(raw_block_trades) if raw_block_trades[index - 1]['token0']['id'] == base_token.address.address: previous_base_token_reserves = Wad.from_number(raw_block_trades[index - 2]['reserve1']) else: previous_base_token_reserves = Wad.from_number(raw_block_trades[index - 2]['reserve0']) timestamp = self.web3.eth.getBlock(checked_block).timestamp trades_list.append(UniswapTrade.from_our_trades_message(block_trade, pair, base_token, our_liquidity_balance, previous_base_token_reserves, timestamp)) checked_block += self.number_of_blocks_to_check # Avoid excessively querying the api by storing the block of the last retrieved trade if len(trades_list) > 0: self.our_last_pair_hour_block = end_block return trades_list def get_all_trades(self, pair: str, page_number: int = 1) -> List[Trade]: assert (isinstance(pair, str)) assert (isinstance(page_number, int)) base_token, quote_token = self.instantiate_tokens(pair) pair_address = self.get_pair_address(base_token.address, quote_token.address) trades_list = [] # use the last retrieved from trade-service as a starting point to avoid duplicate trade syncing if self.start_blocks: if self.start_blocks[pair] > self.all_last_pair_hour_block: self.all_last_pair_hour_block = self.start_blocks[pair] # calculate starting block, assuming there's 15 seconds in a given block current_block = self.get_current_block() one_day_ago_block = int(current_block - (4 * 60 * 24)) start_block = one_day_ago_block if one_day_ago_block > self.all_last_pair_hour_block else self.all_last_pair_hour_block end_block = current_block raw_block_trades = [] checked_block = start_block while checked_block + self.number_of_blocks_to_check < end_block: # Query previous time slice to provide a comparison point for reserve amount changes if len(raw_block_trades) == 0: block_trade = self.get_block_trade(pair_address, checked_block - self.number_of_blocks_to_check)[0] raw_block_trades.append(block_trade) block_trade = self.get_block_trade(pair_address, checked_block)[0] raw_block_trades.append(block_trade) # use len of raw_block_trades to access previous trades index = len(raw_block_trades) if raw_block_trades[index - 1]['token0']['id'] == base_token.address.address: previous_base_token_reserves = Wad.from_number(raw_block_trades[index - 2]['reserve1']) else: previous_base_token_reserves = Wad.from_number(raw_block_trades[index - 2]['reserve0']) timestamp = self.web3.eth.getBlock(checked_block).timestamp trades_list.append(UniswapTrade.from_all_trades_message(block_trade, pair, base_token, previous_base_token_reserves, timestamp)) checked_block += self.number_of_blocks_to_check # Avoid excessively querying the api by storing the timestamp of the last retrieved trade if len(trades_list) > 0: self.all_last_pair_hour_block = end_block return trades_list def _deadline(self) -> int: """Get a predefined deadline.""" return int(time.time()) + 1000 def __eq__(self, other): assert (isinstance(other, UniswapExchange)) return self.address == other.address def __repr__(self): return f"UniswapV2Analytics"
class IDEX(Contract): """A client for the IDEX Exchange contract. You can find the source code of the IDEX Exchange contract here: <https://etherscan.io/address/0x2a0c0dbecc7e4d658f48e01e3fa353f44050c208#code>. Some API docs can be found here: <https://github.com/AuroraDAO/idex-api-docs>. Attributes: web3: An instance of `Web` from `web3.py`. address: Ethereum address of the IDEX Exchange contract. """ abi = Contract._load_abi(__name__, 'abi/IDEX.abi') bin = Contract._load_bin(__name__, 'abi/IDEX.bin') ETH_TOKEN = Address("0x0000000000000000000000000000000000000000") @staticmethod def deploy(web3: Web3, fee_account: Address): """Deploy a new instance of the IDEX Exchange contract. Args: web3: An instance of `Web` from `web3.py`. fee_account: The address of the account which will collect fees. Returns: An `IDEX` class instance. """ return IDEX(web3=web3, address=Contract._deploy(web3, IDEX.abi, IDEX.bin, [fee_account.address])) 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) 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 approve(self, tokens: List[ERC20Token], approval_function): """Approve the IDEX Exchange 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, 'IDEX Exchange contract') def deposit(self, amount: Wad) -> Transact: """Deposits `amount` of raw ETH to IDEX. Args: amount: Amount of raw ETH to be deposited on IDEX. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(amount, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'deposit', [], {'value': amount.value}) def withdraw(self, amount: Wad) -> Transact: """Withdraws `amount` of raw ETH from IDEX. The withdrawn ETH will get transferred to the calling account. Args: amount: Amount of raw ETH to be withdrawn from IDEX. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(amount, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'withdraw', [self.ETH_TOKEN.address, 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 IDEX Exchange contract by the specified user. """ assert (isinstance(user, Address)) return Wad(self._contract.call().balanceOf(self.ETH_TOKEN.address, user.address)) def deposit_token(self, token: Address, amount: Wad) -> Transact: """Deposits `amount` of ERC20 token `token` to IDEX. Tokens will be pulled from the calling account, so the IDEX contract needs to have appropriate allowance. Either call `approve()` or set the allowance manually before trying to deposit tokens. Args: token: Address of the ERC20 token to be deposited. amount: Amount of token `token` to be deposited to IDEX. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(token, Address)) assert (isinstance(amount, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'depositToken', [token.address, amount.value]) def withdraw_token(self, token: Address, amount: Wad) -> Transact: """Withdraws `amount` of ERC20 token `token` from IDEX. 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 IDEX. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ assert (isinstance(token, Address)) assert (isinstance(amount, Wad)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'withdraw', [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 IDEX contract by the specified user. """ assert (isinstance(token, Address)) assert (isinstance(user, Address)) return Wad(self._contract.call().balanceOf(token.address, user.address)) def __repr__(self): return f"IDEX('{self.address}')"
def deploy(web3: Web3, pauseAddress: Address, vatAddress: Address): return DSSSpell(web3=web3, address=Contract._deploy( web3, DSSSpell.abi, DSSSpell.bin, [pauseAddress.address, vatAddress.address]))
class DsrManager(Contract): """ A client for the `DsrManger` contract, which reduces the need for proxies when interacting with the Pot contract. Ref. <https://github.com/makerdao/dsr-manager/blob/master/src/DsrManager.sol> """ abi = Contract._load_abi(__name__, 'abi/DsrManager.abi') bin = Contract._load_bin(__name__, 'abi/DsrManager.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) def pot(self) -> Pot: address = Address(self._contract.functions.pot().call()) return Pot(self.web3, address) def dai(self) -> DSToken: address = Address(self._contract.functions.dai().call()) return DSToken(self.web3, address) def dai_adapter(self) -> DaiJoin: address = Address(self._contract.functions.daiJoin().call()) return DaiJoin(self.web3, address) def supply(self) -> Wad: """Total supply of pie locked in Pot through DsrManager""" return Wad(self._contract.functions.supply().call()) def pie_of(self, usr: Address) -> Wad: """Pie balance of a given usr address""" assert isinstance(usr, Address) return Wad(self._contract.functions.pieOf(usr.address).call()) def dai_of(self, usr: Address) -> Rad: """ Internal Dai balance of a given usr address - current Chi is used i.e. Dai balance potentially stale """ assert isinstance(usr, Address) pie = self.pie_of(usr) chi = self.pot().chi() dai = Rad(pie) * Rad(chi) return dai def join(self, dst: Address, dai: Wad) -> Transact: """Lock a given amount of ERC20 Dai into the DSR Contract and give to dst address """ assert isinstance(dst, Address) assert isinstance(dai, Wad) return Transact(self, self.web3, self.abi, self.address, self._contract, 'join', [dst.address, dai.value]) def exit(self, dst: Address, dai: Wad) -> Transact: """ Free a given amount of ERC20 Dai from the DSR Contract and give to dst address """ assert isinstance(dst, Address) assert isinstance(dai, Wad) return Transact(self, self.web3, self.abi, self.address, self._contract, 'exit', [dst.address, dai.value]) def exitAll(self, dst: Address) -> Transact: """ Free all ERC20 Dai from the DSR Contract and give to dst address """ assert isinstance(dst, Address) return Transact(self, self.web3, self.abi, self.address, self._contract, 'exitAll', [dst.address]) def __repr__(self): return f"DsrManager('{self.address}')"
class TxManager(Contract): """A client for the `TxManager` contract. `TxManager` allows to invoke multiple contract methods in one Ethereum transaction. Each invocation is represented as an instance of the `Invocation` class, containing a contract address and a calldata. In addition to that, these invocations can use ERC20 token balances. In order to do that, the entire allowance of each token involved is transferred from the caller to the `TxManager` contract at the beginning of the transaction and all the remaining balances are returned to the caller at the end of it. In order to use this feature, ERC20 token allowances have to be granted to the `TxManager`. You can find the source code of the `TxManager` contract here: <https://github.com/makerdao/tx-manager>. Attributes: web3: An instance of `Web` from `web3.py`. address: Ethereum address of the `TxManager` contract. """ abi = Contract._load_abi(__name__, 'abi/TxManager.abi') bin = Contract._load_bin(__name__, 'abi/TxManager.bin') def __init__(self, web3: Web3, address: Address): 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): return TxManager(web3=web3, address=Contract._deploy(web3, TxManager.abi, TxManager.bin, [])) def approve(self, tokens: List[ERC20Token], approval_function): """Approve the TxManager 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, 'TxManager') def owner(self) -> Address: return Address(self._contract.functions.owner().call()) def execute(self, tokens: List[Address], invocations: List[Invocation]) -> Transact: """Executes multiple contract methods in one Ethereum transaction. Args: tokens: List of addresses of ERC20 token the invocations should be able to access. invocations: A list of invocations (contract methods) to be executed. Returns: A :py:class:`pymaker.Transact` instance, which can be used to trigger the transaction. """ def token_addresses() -> list: return list(map(lambda address: address.address, tokens)) def script() -> bytes: return reduce( operator.add, map(lambda invocation: script_entry(invocation), invocations), bytes()) def script_entry(invocation: Invocation) -> bytes: address = invocation.address.as_bytes() calldata = invocation.calldata.as_bytes() calldata_length = len(calldata).to_bytes(32, byteorder='big') return address + calldata_length + calldata assert (isinstance(tokens, list)) assert (isinstance(invocations, list)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'execute', [token_addresses(), script()]) def __repr__(self): return f"TxManager('{self.address}')"
class ZrxExchange(Contract): """A client for the 0x exchange contract. You can find the source code of the `0x` exchange contract here: <https://etherscan.io/address/0x12459c951127e0c374ff9105dda097662a027093#code. Attributes: web3: An instance of `Web` from `web3.py`. address: Ethereum address of the _0x_ `Exchange` contract. """ abi = Contract._load_abi(__name__, 'abi/Exchange.abi') bin = Contract._load_bin(__name__, 'abi/Exchange.bin') _ZERO_ADDRESS = Address("0x0000000000000000000000000000000000000000") @staticmethod def deploy(web3: Web3, zrx_token: Address, token_transfer_proxy: Address): """Deploy a new instance of the 0x `Exchange` contract. Args: web3: An instance of `Web` from `web3.py`. zrx_token: The address of the ZRX token this exchange will use. token_transfer_proxy: The address of the token transfer proxy this exchange will use. Returns: A `ZrxExchange` class instance. """ return ZrxExchange( web3=web3, address=Contract._deploy( web3, ZrxExchange.abi, ZrxExchange.bin, [zrx_token.address, token_transfer_proxy.address])) 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) def zrx_token(self) -> Address: """Get the address of the ZRX token contract associated with this `Exchange` contract. Returns: The address of the `ZRX` token. """ return Address(self._contract.call().ZRX_TOKEN_CONTRACT()) def token_transfer_proxy(self) -> Address: """Get the address of the `TokenTransferProxy` contract associated with this `Exchange` contract. Returns: The address of the `TokenTransferProxy` token. """ return Address(self._contract.call().TOKEN_TRANSFER_PROXY_CONTRACT()) def approve(self, tokens: List[ERC20Token], approval_function): """Approve the 0x Exchange TokenTransferProxy contract to fully access balances of specified tokens. In case of 0x, it's the TokenTransferProxy contract that actually gets the approvals, not the 0x Exchange contract itself. In addition to the tokens specified as the `tokens` parameter, the ZRX token always gets approved as well as without it the 0x Exchange contract wouldn't be able to charge maker and taker fees. 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 + [ ERC20Token(web3=self.web3, address=self.zrx_token()) ]: approval_function(token, self.token_transfer_proxy(), '0x Exchange contract') def create_order(self, pay_token: Address, pay_amount: Wad, buy_token: Address, buy_amount: Wad, expiration: int): assert (isinstance(pay_token, Address)) assert (isinstance(pay_amount, Wad)) assert (isinstance(buy_token, Address)) assert (isinstance(buy_amount, Wad)) assert (isinstance(expiration, int)) return Order(exchange=self, maker=Address(self.web3.eth.defaultAccount), taker=self._ZERO_ADDRESS, maker_fee=Wad(0), taker_fee=Wad(0), pay_token=pay_token, pay_amount=pay_amount, buy_token=buy_token, buy_amount=buy_amount, salt=self.random_salt(), fee_recipient=self._ZERO_ADDRESS, expiration=expiration, exchange_contract_address=self.address, ec_signature_r=None, ec_signature_s=None, ec_signature_v=None) def get_order_hash(self, order: Order) -> str: assert (isinstance(order, Order)) # the hash depends on the exchange contract address as well assert (order.exchange_contract_address == self.address) result = self._contract.call().getOrderHash( self._order_addresses(order), self._order_values(order)) return bytes_to_hexstring( array.array('B', [ord(x) for x in result]).tobytes()) def get_unavailable_buy_amount(self, order: Order) -> Wad: assert (isinstance(order, Order)) return Wad(self._contract.call().getUnavailableTakerTokenAmount( hexstring_to_bytes(self.get_order_hash(order)))) def sign_order(self, order: Order) -> Order: assert (isinstance(order, Order)) # TODO duplicate code below signed_hash = eth_sign(self.web3, hexstring_to_bytes( self.get_order_hash(order)))[2:] r = bytes.fromhex(signed_hash[0:64]) s = bytes.fromhex(signed_hash[64:128]) v = ord(bytes.fromhex(signed_hash[128:130])) signed_order = copy.copy(order) signed_order.ec_signature_r = bytes_to_hexstring(r) signed_order.ec_signature_s = bytes_to_hexstring(s) signed_order.ec_signature_v = v return signed_order def cancel_order(self, order: Order) -> Transact: assert (isinstance(order, Order)) return Transact(self, self.web3, self.abi, self.address, self._contract, 'cancelOrder', [ self._order_addresses(order), self._order_values(order), order.buy_amount.value ]) @staticmethod def _order_values(order): return [ order.pay_amount.value, order.buy_amount.value, order.maker_fee.value, order.taker_fee.value, order.expiration, order.salt ] @staticmethod def _order_addresses(order): return [ order.maker.address, order.taker.address, order.pay_token.address, order.buy_token.address, order.fee_recipient.address ] @staticmethod def random_salt() -> int: return random.randint(1, 2**256 - 1) def __repr__(self): return f"ZrxExchange('{self.address}')"
class AirswapContract(Contract): """A client for a `Airswap` contract. `AirwapContract` is a simple on-chain OTC market for ERC20-compatible tokens. Attributes: web3: An instance of `Web` from `web3.py`. address: Ethereum address of the `Airswap` contract. past_blocks: Number of past ethereum blocks to query """ abi = Contract._load_abi(__name__, 'abi/Airswap.abi') bin = Contract._load_bin(__name__, 'abi/Airswap.bin') def __init__(self, web3: Web3, address: Address, past_blocks): assert (isinstance(web3, Web3)) assert (isinstance(address, Address)) self.web3 = web3 self.address = address self.past_blocks = past_blocks self._contract = self._get_contract(web3, self.abi, address) def past_fill(self, number_of_past_blocks: int, event_filter: dict = None) -> List[Filled]: """Synchronously retrieve past Fill events. `Fill` events are emitted by the Airswap contract every time someone fills and 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 `Fill` 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, 'Filled', Filled, number_of_past_blocks, event_filter) def get_trades(self, pair, page_number: int = 1): assert (isinstance(page_number, int)) fills = self.get_all_trades(pair, page_number) address = Address(self.web3.eth.defaultAccount) # Filter trades from address fills = [ fill for fill in fills if fill.maker == address or fill.taker == address ] return fills def get_all_trades(self, pair, page_number: int = 1): assert (page_number == 1) fills = self.past_fill(self.past_blocks) # Filter trades for addresses in pair fills = [ fill for fill in fills if fill.maker_token in pair and fill.taker_token in pair ] return fills