class AuctionKeeper: logger = logging.getLogger() dead_after = 10 # Assume block reorgs cannot resurrect an auction id after this many blocks def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='auction-keeper') parser.add_argument("--rpc-host", type=str, default="http://*****:*****@{output.price} for auction {id}") auction.price = bid_price auction.gas_price = new_gas_strategy if new_gas_strategy else auction.gas_price auction.register_transaction(bid_transact) # ...submit a new transaction and wait the delay period (if so configured) self._run_future(bid_transact.transact_async(gas_price=auction.gas_price)) if self.arguments.bid_delay: logging.debug(f"Waiting {self.arguments.bid_delay}s") time.sleep(self.arguments.bid_delay) # if transaction in progress and the bid price changed... elif auction.price and bid_price != auction.price: self.logger.info(f"Attempting to override pending bid with new bid @{output.price} for auction {id}") auction.price = bid_price if new_gas_strategy: # gas strategy changed auction.gas_price = new_gas_strategy elif fixed_gas_price_changed: # gas price updated assert isinstance(auction.gas_price, UpdatableGasPrice) auction.gas_price.update_gas_price(output.gas_price) auction.register_transaction(bid_transact) # ...ask pymaker to replace the transaction self._run_future(bid_transact.transact_async(replace=transaction_in_progress, gas_price=auction.gas_price)) # if model has been providing a gas price, and only that changed... elif fixed_gas_price_changed: assert isinstance(auction.gas_price, UpdatableGasPrice) self.logger.info(f"Overriding pending bid with new gas_price ({output.gas_price}) for auction {id}") auction.gas_price.update_gas_price(output.gas_price) # if transaction in progress, bid price unchanged, but gas strategy changed... elif new_gas_strategy: self.logger.info(f"Changing gas strategy for pending bid @{output.price} for auction {id}") auction.price = bid_price auction.gas_price = new_gas_strategy auction.register_transaction(bid_transact) # ...ask pymaker to replace the transaction self._run_future(bid_transact.transact_async(replace=transaction_in_progress, gas_price=auction.gas_price)) def check_bid_cost(self, id: int, cost: Rad, reservoir: Reservoir, already_rebalanced=False) -> bool: assert isinstance(id, int) assert isinstance(cost, Rad) # If this is an auction where we bid with Dai... if self.flipper or self.flopper: if not reservoir.check_bid_cost(id, cost): if not already_rebalanced: # Try to synchronously join Dai the Vat if self.is_joining_dai: self.logger.info(f"Bid cost {str(cost)} exceeds reservoir level of {reservoir.level}; " "waiting for Dai to rebalance") return False else: rebalanced = self.rebalance_dai() if rebalanced and rebalanced > Wad(0): reservoir.refill(Rad(rebalanced)) return self.check_bid_cost(id, cost, reservoir, already_rebalanced=True) self.logger.info(f"Bid cost {str(cost)} exceeds reservoir level of {reservoir.level}; " "bid will not be submitted") return False # If this is an auction where we bid with MKR... elif self.flapper: mkr_balance = self.mkr.balance_of(self.our_address) if cost > Rad(mkr_balance): self.logger.debug(f"Bid cost {str(cost)} exceeds reservoir level of {reservoir.level}; " "bid will not be submitted") return False return True def rebalance_dai(self) -> Optional[Wad]: # Returns amount joined (positive) or exited (negative) as a result of rebalancing towards vat_dai_target if self.arguments.vat_dai_target is None: return None logging.info(f"Checking if internal Dai balance needs to be rebalanced") dai = self.dai_join.dai() token_balance = dai.balance_of(self.our_address) # Wad # Prevent spending gas on small rebalances dust = Wad(self.mcd.vat.ilk(self.ilk.name).dust) if self.ilk else Wad.from_number(20) dai_to_join = Wad(0) dai_to_exit = Wad(0) try: if self.arguments.vat_dai_target.upper() == "ALL": dai_to_join = token_balance else: dai_target = Wad.from_number(float(self.arguments.vat_dai_target)) if dai_target < dust: self.logger.warning(f"Dust cutoff of {dust} exceeds Dai target {dai_target}; " "please adjust configuration accordingly") vat_balance = Wad(self.vat.dai(self.our_address)) if vat_balance < dai_target: dai_to_join = dai_target - vat_balance elif vat_balance > dai_target: dai_to_exit = vat_balance - dai_target except ValueError: raise ValueError("Unsupported --vat-dai-target") if dai_to_join >= dust: # Join tokens to the vat if token_balance >= dai_to_join: self.logger.info(f"Joining {str(dai_to_join)} Dai to the Vat") return self.join_dai(dai_to_join) elif token_balance > Wad(0): self.logger.warning(f"Insufficient balance to maintain Dai target; joining {str(token_balance)} " "Dai to the Vat") return self.join_dai(token_balance) else: self.logger.warning("Insufficient Dai is available to join to Vat; cannot maintain Dai target") return Wad(0) elif dai_to_exit > dust: # Exit dai from the vat self.logger.info(f"Exiting {str(dai_to_exit)} Dai from the Vat") assert self.dai_join.exit(self.our_address, dai_to_exit).transact(gas_price=self.gas_price) return dai_to_exit * -1 self.logger.info(f"Dai token balance: {str(dai.balance_of(self.our_address))}, " f"Vat balance: {self.vat.dai(self.our_address)}") def join_dai(self, amount: Wad): assert isinstance(amount, Wad) assert not self.is_joining_dai try: self.is_joining_dai = True assert self.dai_join.join(self.our_address, amount).transact(gas_price=self.gas_price) finally: self.is_joining_dai = False return amount def exit_gem(self): if not self.collateral: return token = Token(self.collateral.ilk.name.split('-')[0], self.collateral.gem.address, self.collateral.adapter.dec()) vat_balance = self.vat.gem(self.ilk, self.our_address) if vat_balance > token.min_amount: self.logger.info(f"Exiting {str(vat_balance)} {self.ilk.name} from the Vat") assert self.gem_join.exit(self.our_address, token.unnormalize_amount(vat_balance)).transact(gas_price=self.gas_price) @staticmethod def _run_future(future): def worker(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: asyncio.get_event_loop().run_until_complete(future) finally: loop.close() thread = threading.Thread(target=worker, daemon=True) thread.start()