예제 #1
0
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()