예제 #1
0
    def test_should_always_be_default(self):
        # given
        default_gas_price = DefaultGasPrice()

        # expect
        assert default_gas_price.get_gas_price(0) is None
        assert default_gas_price.get_gas_price(1) is None
        assert default_gas_price.get_gas_price(1000000) is None
예제 #2
0
 def create_gas_price(arguments) -> GasPrice:
     if arguments.smart_gas_price:
         return SmartGasPrice()
     elif arguments.gas_price:
         return FixedGasPrice(arguments.gas_price)
     else:
         return DefaultGasPrice()
예제 #3
0
 def create_gas_price(web3: Web3, arguments: Namespace) -> GasPrice:
     if arguments.smart_gas_price:
         return SmartGasPrice(arguments.ethgasstation_api_key)
     elif arguments.dynamic_gas_price:
         return DynamicGasPrice(web3, arguments)
     else:
         return DefaultGasPrice()
예제 #4
0
 def create_gas_price(arguments) -> GasPrice:
     if arguments.smart_gas_price:
         return SmartGasPrice(arguments.ethgasstation_api_key)
     elif arguments.gas_price:
         return FixedGasPrice(arguments.gas_price)
     else:
         return DefaultGasPrice()
예제 #5
0
파일: dss.py 프로젝트: ith-harvey/pymaker
    def approve(self, usr: Address, **kwargs):
        """
        Allows the user to move this collateral into and out of their CDP.

        Args
            usr: User making transactions with this collateral
        """
        gas_price = kwargs['gas_price'] if 'gas_price' in kwargs else DefaultGasPrice()
        self.adapter.approve(hope_directly(from_address=usr, gas_price=gas_price), self.flipper.vat())
        self.adapter.approve_token(directly(from_address=usr, gas_price=gas_price))
예제 #6
0
    def approve_dai(self, usr: Address, **kwargs):
        """
        Allows the user to draw Dai from and repay Dai to their CDPs.

        Args
            usr: Recipient of Dai from one or more CDPs
        """
        assert isinstance(usr, Address)

        gas_price = kwargs['gas_price'] if 'gas_price' in kwargs else DefaultGasPrice()
        self.dai_adapter.approve(approval_function=hope_directly(from_address=usr, gas_price=gas_price),
                                 source=self.vat.address)
        self.dai.approve(self.dai_adapter.address).transact(from_address=usr, gas_price=gas_price)
예제 #7
0
 def create_gas_price(arguments) -> GasPrice:
     if arguments.smart_gas_price:
         return SmartGasPrice()
     elif arguments.gas_price_file:
         return GasPriceFile(arguments.gas_price_file)
     elif arguments.gas_price:
         if arguments.gas_price_increase is not None:
             return IncreasingGasPrice(initial_price=arguments.gas_price,
                                       increase_by=arguments.gas_price_increase,
                                       every_secs=arguments.gas_price_increase_every,
                                       max_price=arguments.gas_price_max)
         else:
             return FixedGasPrice(arguments.gas_price)
     else:
         return DefaultGasPrice()
예제 #8
0
    def get_gas_price(self, time_elapsed: int) -> Optional[int]:
        assert(isinstance(time_elapsed, int))

        config = self.reloadable_config.get_config()
        gas_price = config.get('gasPrice', None)
        gas_price_increase = config.get('gasPriceIncrease', None)
        gas_price_increase_every = config.get('gasPriceIncreaseEvery', None)
        gas_price_max = config.get('gasPriceMax', None)

        if gas_price is not None:
            if gas_price_increase and gas_price_increase_every:
                strategy = IncreasingGasPrice(gas_price, gas_price_increase, gas_price_increase_every, gas_price_max)
            else:
                strategy = FixedGasPrice(gas_price)
        else:
            strategy = DefaultGasPrice()

        return strategy.get_gas_price(time_elapsed=time_elapsed)
예제 #9
0
    def __init__(self, args: list, **kwargs):
        """Pass in arguements assign necessary variables/objects and instantiate other Classes"""

        parser = argparse.ArgumentParser("cage-keeper")
        self.add_arguments(parser=parser)

        parser.set_defaults(cageFacilitated=False)
        self.arguments = parser.parse_args(args)

        # Configure connection to the chain
        provider = HTTPProvider(
            endpoint_uri=self.arguments.rpc_host,
            request_kwargs={'timeout': self.arguments.rpc_timeout})

        self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            provider)

        self.web3.eth.defaultAccount = self.arguments.eth_from
        register_keys(self.web3, self.arguments.eth_key)
        self.our_address = Address(self.arguments.eth_from)

        if self.arguments.dss_deployment_file:
            self.dss = DssDeployment.from_json(
                web3=self.web3,
                conf=open(self.arguments.dss_deployment_file, "r").read())
        else:
            self.dss = DssDeployment.from_node(web3=self.web3)

        self.deployment_block = self.arguments.vat_deployment_block

        self.max_errors = self.arguments.max_errors
        self.errors = 0

        self.cageFacilitated = self.arguments.cageFacilitated

        self.confirmations = 0

        # Create gas strategy
        if self.arguments.ethgasstation_api_key:
            self.gas_price = DynamicGasPrice(self.arguments)
        else:
            self.gas_price = DefaultGasPrice()

        setup_logging(self.arguments)
예제 #10
0
    def check_auction(self, id: int) -> bool:
        assert isinstance(id, int)

        if id in self.dead_auctions:
            return False

        # Read auction information
        input = self.strategy.get_input(id)
        auction_missing = (input.end == 0)
        auction_finished = (input.tic < input.era
                            and input.tic != 0) or (input.end < input.era)

        if auction_missing:
            # Try to remove the auction so the model terminates and we stop tracking it.
            # If auction has already been removed, nothing happens.
            self.auctions.remove_auction(id)
            self.dead_auctions.add(id)
            return False

        # Check if the auction is finished.
        # If it is finished and we are the winner, `deal` the auction.
        # If it is finished and we aren't the winner, there is no point in carrying on with this auction.
        elif auction_finished:
            if input.guy == self.our_address:
                # Always using default gas price for `deal`
                self._run_future(
                    self.strategy.deal(id).transact_async(
                        gas_price=DefaultGasPrice()))

                # Upon winning a flip or flop auction, we may need to replenish Dai to the Vat.
                # Upon winning a flap auction, we may want to withdraw won Dai from the Vat.
                self.rebalance_dai()

            else:
                # Try to remove the auction so the model terminates and we stop tracking it.
                # If auction has already been removed, nothing happens.
                self.auctions.remove_auction(id)
            return False

        else:
            return True
예제 #11
0
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser("chief-keeper")
        self.add_arguments(parser)
        parser.set_defaults(cageFacilitated=False)
        self.arguments = parser.parse_args(args)

        provider = HTTPProvider(
            endpoint_uri=self.arguments.rpc_host,
            request_kwargs={'timeout': self.arguments.rpc_timeout})
        self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            provider)

        self.web3.eth.defaultAccount = self.arguments.eth_from
        register_keys(self.web3, self.arguments.eth_key)
        self.our_address = Address(self.arguments.eth_from)

        if self.arguments.dss_deployment_file:
            self.dss = DssDeployment.from_json(
                web3=self.web3,
                conf=open(self.arguments.dss_deployment_file, "r").read())
        else:
            self.dss = DssDeployment.from_node(web3=self.web3)

        self.deployment_block = self.arguments.chief_deployment_block

        self.max_errors = self.arguments.max_errors
        self.errors = 0

        self.confirmations = 0

        if self.arguments.fixed_gas_price is not None and self.arguments.fixed_gas_price > 0:
            self.gas_price_strategy = FixedGasPrice(
                gas_price=int(round(self.arguments.fixed_gas_price *
                                    self.GWEI)))
        else:
            self.gas_price_strategy = DefaultGasPrice()

        setup_logging(self.arguments)
예제 #12
0
"""


if len(sys.argv) > 3:
    web3.eth.defaultAccount = sys.argv[2]
    register_keys(web3, [sys.argv[3]])
    our_address = Address(web3.eth.defaultAccount)
    run_transactions = True
elif len(sys.argv) > 2:
    our_address = Address(sys.argv[2])
    run_transactions = False
else:
    our_address = None
    run_transactions = False

gas_strategy = DefaultGasPrice() if len(sys.argv) <= 4 else \
    GeometricGasPrice(web3=web3,
                      initial_price=None,
                      initial_tip=int(float(sys.argv[4]) * GeometricGasPrice.GWEI),
                      every_secs=5,
                      max_price=50 * GeometricGasPrice.GWEI)

eth = EthToken(web3, Address.zero())


class TestApp:
    def main(self):
        with Lifecycle(web3) as lifecycle:
            lifecycle.on_block(self.on_block)

    def on_block(self):
예제 #13
0
    async def transact_async(self, **kwargs) -> Optional[Receipt]:
        """Executes the Ethereum transaction asynchronously.

        Executes the Ethereum transaction asynchronously. The method will return immediately.
        Ultimately, its future value will become either a :py:class:`pymaker.Receipt` or `None`,
        depending on whether the transaction execution was successful or not.

        Out-of-gas exceptions are automatically recognized as transaction failures.

        Allowed keyword arguments are: `from_address`, `replace`, `gas`, `gas_buffer`, `gas_price`.
        `gas_price` needs to be an instance of a class inheriting from :py:class:`pymaker.gas.GasPrice`.

        The `gas` keyword argument is the gas limit for the transaction, whereas `gas_buffer`
        specifies how much gas should be added to the estimate. They can not be present
        at the same time. If none of them are present, a default buffer is added to the estimate.

        Returns:
            A future value of either a :py:class:`pymaker.Receipt` object if the transaction
            invocation was successful, or `None` if it failed.
        """

        unknown_kwargs = set(kwargs.keys()) - {'from_address', 'replace', 'gas', 'gas_buffer', 'gas_price'}
        if len(unknown_kwargs) > 0:
            raise Exception(f"Unknown kwargs: {unknown_kwargs}")

        # Get the from account.
        from_account = kwargs['from_address'].address if ('from_address' in kwargs) else self.web3.eth.defaultAccount

        # First we try to estimate the gas usage of the transaction. If gas estimation fails
        # it means there is no point in sending the transaction, thus we fail instantly and
        # do not increment the nonce. If the estimation is successful, we pass the calculated
        # gas value (plus some `gas_buffer`) to the subsequent `transact` calls so it does not
        # try to estimate it again.
        try:
            gas_estimate = self.estimated_gas(Address(from_account))
        except:
            self.logger.warning(f"Transaction {self.name()} will fail, refusing to send ({sys.exc_info()[1]})")
            return None

        # Get or calculate `gas`. Get `gas_price`, which in fact refers to a gas pricing algorithm.
        gas = self._gas(gas_estimate, **kwargs)
        gas_price = kwargs['gas_price'] if ('gas_price' in kwargs) else DefaultGasPrice()
        assert(isinstance(gas_price, GasPrice))

        # Get the transaction this one is supposed to replace.
        # If there is one, try to borrow the nonce from it as long as that transaction isn't finished.
        replaced_tx = kwargs['replace'] if ('replace' in kwargs) else None
        if replaced_tx is not None:
            while replaced_tx.nonce is None and replaced_tx.status != TransactStatus.FINISHED:
                await asyncio.sleep(0.25)

            self.nonce = replaced_tx.nonce

        # Initialize variables which will be used in the main loop.
        tx_hashes = []
        initial_time = time.time()
        gas_price_last = 0

        while True:
            seconds_elapsed = int(time.time() - initial_time)

            if self.nonce is not None and self.web3.eth.getTransactionCount(from_account) > self.nonce:
                # Check if any transaction sent so far has been mined (has a receipt).
                # If it has, we return either the receipt (if if was successful) or `None`.
                for attempt in range(1, 11):
                    for tx_hash in tx_hashes:
                        receipt = self._get_receipt(tx_hash)
                        if receipt:
                            if receipt.successful:
                                self.logger.info(f"Transaction {self.name()} was successful (tx_hash={bytes_to_hexstring(tx_hash)})")
                                return receipt
                            else:
                                self.logger.warning(f"Transaction {self.name()} mined successfully but generated no single"
                                                    f" log entry, assuming it has failed (tx_hash={bytes_to_hexstring(tx_hash)})")
                                return None

                    self.logger.debug(f"No receipt found in attempt #{attempt}/10 (nonce={self.nonce},"
                                      f" getTransactionCount={self.web3.eth.getTransactionCount(from_account)})")

                    await asyncio.sleep(0.5)

                # If we can not find a mined receipt but at the same time we know last used nonce
                # has increased, then it means that the transaction we tried to send failed.
                self.logger.warning(f"Transaction {self.name()} has been overridden by another transaction"
                                    f" with the same nonce, which means it has failed")
                return None

            # Send a transaction if:
            # - no transaction has been sent yet, or
            # - the gas price requested has changed since the last transaction has been sent
            gas_price_value = gas_price.get_gas_price(seconds_elapsed)
            if len(tx_hashes) == 0 or ((gas_price_value is not None) and (gas_price_last is not None) and
                                           (gas_price_value > gas_price_last * 1.1)):
                gas_price_last = gas_price_value

                try:
                    # We need the lock in order to not try to send two transactions with the same nonce.
                    with transaction_lock:
                        if self.nonce is None:
                            if self._is_parity():
                                self.nonce = int(self.web3.manager.request_blocking("parity_nextNonce", [from_account]), 16)

                            else:
                                self.nonce = self.web3.eth.getTransactionCount(from_account, block_identifier='pending')

                        tx_hash = self._func(from_account, gas, gas_price_value, self.nonce)
                        tx_hashes.append(tx_hash)

                    self.logger.info(f"Sent transaction {self.name()} with nonce={self.nonce}, gas={gas},"
                                     f" gas_price={gas_price_value if gas_price_value is not None else 'default'}"
                                     f" (tx_hash={bytes_to_hexstring(tx_hash)})")
                except Exception as e:
                    self.logger.warning(f"Failed to send transaction {self.name()} with nonce={self.nonce}, gas={gas},"
                                        f" gas_price={gas_price_value if gas_price_value is not None else 'default'}"
                                        f" ({e})")

                    if len(tx_hashes) == 0:
                        raise

            await asyncio.sleep(0.25)
 def gas_price(self):
     """ FixedGasPrice if gas_price argument present, otherwise node DefaultGasPrice """
     if self.arguments.gas_price > 0:
         return FixedGasPrice(self.arguments.gas_price)
     else:
         return DefaultGasPrice()
예제 #15
0
    def __init__(self, args: list, **kwargs):
        """Pass in arguements assign necessary variables/objects and instantiate other Classes"""

        parser = argparse.ArgumentParser("cage-keeper")

        parser.add_argument("--rpc-host",
                            type=str,
                            default="localhost",
                            help="JSON-RPC host (default: `localhost')")

        parser.add_argument("--rpc-port",
                            type=int,
                            default=8545,
                            help="JSON-RPC port (default: `8545')")

        parser.add_argument("--rpc-timeout",
                            type=int,
                            default=1200,
                            help="JSON-RPC timeout (in seconds, default: 10)")

        parser.add_argument(
            "--network",
            type=str,
            required=True,
            help=
            "Network that you're running the Keeper on (options, 'mainnet', 'kovan', 'testnet')"
        )

        parser.add_argument(
            '--previous-cage',
            dest='cageFacilitated',
            action='store_true',
            help=
            'Include this argument if this keeper previously helped to facilitate the processing phase of ES'
        )

        parser.add_argument(
            "--eth-from",
            type=str,
            required=True,
            help=
            "Ethereum address from which to send transactions; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--eth-key",
            type=str,
            nargs='*',
            help=
            "Ethereum private key(s) to use (e.g. 'key_file=/path/to/keystore.json,pass_file=/path/to/passphrase.txt')"
        )

        parser.add_argument(
            "--dss-deployment-file",
            type=str,
            required=False,
            help=
            "Json description of all the system addresses (e.g. /Full/Path/To/configFile.json)"
        )

        parser.add_argument(
            "--vat-deployment-block",
            type=int,
            required=False,
            default=0,
            help=
            " Block that the Vat from dss-deployment-file was deployed at (e.g. 8836668"
        )

        parser.add_argument(
            "--vulcanize-endpoint",
            type=str,
            help=
            "When specified, frob history will be queried from a VulcanizeDB lite node, "
            "reducing load on the Ethereum node for Vault query")

        parser.add_argument("--vulcanize-key",
                            type=str,
                            help="API key for the Vulcanize endpoint")

        parser.add_argument(
            "--max-errors",
            type=int,
            default=100,
            help=
            "Maximum number of allowed errors before the keeper terminates (default: 100)"
        )

        parser.add_argument("--debug",
                            dest='debug',
                            action='store_true',
                            help="Enable debug output")

        parser.add_argument("--ethgasstation-api-key",
                            type=str,
                            default=None,
                            help="ethgasstation API key")

        parser.add_argument("--gas-initial-multiplier",
                            type=str,
                            default=1.0,
                            help="ethgasstation API key")
        parser.add_argument("--gas-reactive-multiplier",
                            type=str,
                            default=2.25,
                            help="gas strategy tuning")
        parser.add_argument("--gas-maximum",
                            type=str,
                            default=5000,
                            help="gas strategy tuning")

        parser.set_defaults(cageFacilitated=False)
        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"https://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                request_kwargs={"timeout": self.arguments.rpc_timeout}))
        self.web3.eth.defaultAccount = self.arguments.eth_from
        register_keys(self.web3, self.arguments.eth_key)
        self.our_address = Address(self.arguments.eth_from)

        if self.arguments.dss_deployment_file:
            self.dss = DssDeployment.from_json(
                web3=self.web3,
                conf=open(self.arguments.dss_deployment_file, "r").read())
        else:
            self.dss = DssDeployment.from_network(
                web3=self.web3, network=self.arguments.network)

        self.deployment_block = self.arguments.vat_deployment_block

        self.max_errors = self.arguments.max_errors
        self.errors = 0

        self.cageFacilitated = self.arguments.cageFacilitated

        self.confirmations = 0

        # Create gas strategy
        if self.arguments.ethgasstation_api_key:
            self.gas_price = DynamicGasPrice(self.arguments, self.web3)
        else:
            self.gas_price = DefaultGasPrice()

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=(logging.DEBUG if self.arguments.debug else logging.INFO))
예제 #16
0
    async def transact_async(self, **kwargs) -> Optional[Receipt]:
        """Executes the Ethereum transaction asynchronously.

        Executes the Ethereum transaction asynchronously. The method will return immediately.
        Ultimately, its future value will become either a :py:class:`pymaker.Receipt` or `None`,
        depending on whether the transaction execution was successful or not.

        Out-of-gas exceptions are automatically recognized as transaction failures.

        Allowed keyword arguments are: `gas`, `gas_buffer`, `gas_price`. `gas_price` needs
        to be an instance of a class inheriting from :py:class:`pymaker.gas.GasPrice`.

        The `gas` keyword argument is the gas limit for the transaction, whereas `gas_buffer`
        specifies how much gas should be added to the estimate. They can not be present
        at the same time. If none of them are present, a default buffer is added to the estimate.

        Returns:
            A future value of either a :py:class:`pymaker.Receipt` object if the transaction
            invocation was successful, or `None` if it failed.
        """

        # Get the from account.
        from_account = kwargs['from_address'].address if ('from_address' in kwargs) else self.web3.eth.defaultAccount

        # First we try to estimate the gas usage of the transaction. If gas estimation fails
        # it means there is no point in sending the transaction, thus we fail instantly and
        # do not increment the nonce. If the estimation is successful, we pass the calculated
        # gas value (plus some `gas_buffer`) to the subsequent `transact` calls so it does not
        # try to estimate it again. If it would try to estimate it again it could turn out
        # this transaction will fail (another block might have been mined in the meantime for
        # example), which would mean we incremented the nonce but never used it.
        #
        # This is why gas estimation has to happen first and before the nonce gets incremented.
        try:
            gas_estimate = self.estimated_gas(Address(from_account))
        except:
            self.logger.warning(f"Transaction {self.name()} will fail, refusing to send ({sys.exc_info()[1]})")
            return None

        # Get or calculate `gas`. Get `gas_price`, which in fact refers to a gas pricing algorithm.
        gas = self._gas(gas_estimate, **kwargs)
        gas_price = kwargs['gas_price'] if ('gas_price' in kwargs) else DefaultGasPrice()
        assert(isinstance(gas_price, GasPrice))

        # Initialize variables which will be used in the main loop.
        nonce = None
        tx_hashes = []
        initial_time = time.time()
        gas_price_last = 0

        while True:
            seconds_elapsed = int(time.time() - initial_time)

            if nonce is not None and self.web3.eth.getTransactionCount(from_account) > nonce:
                # Check if any transaction sent so far has been mined (has a receipt).
                # If it has, we return either the receipt (if if was successful) or `None`.
                for tx_hash in tx_hashes:
                    receipt = self._get_receipt(tx_hash)
                    if receipt:
                        if receipt.successful:
                            self.logger.info(f"Transaction {self.name()} was successful (tx_hash={tx_hash})")
                            return receipt
                        else:
                            self.logger.warning(f"Transaction {self.name()} mined successfully but generated no single"
                                                f" log entry, assuming it has failed (tx_hash={tx_hash})")
                            return None

                # If we can not find a mined receipt but at the same time we know last used nonce
                # has increased, then it means that the transaction we tried to send failed.
                self.logger.warning(f"Transaction {self.name()} has been overridden by another transaction"
                                    f" with the same nonce, which means it has failed")
                return None

            # Send a transaction if:
            # - no transaction has been sent yet, or
            # - the gas price requested has changed since the last transaction has been sent
            gas_price_value = gas_price.get_gas_price(seconds_elapsed)
            if len(tx_hashes) == 0 or ((gas_price_value is not None) and (gas_price_last is not None) and
                                           (gas_price_value > gas_price_last)):
                gas_price_last = gas_price_value

                try:
                    tx_hash = self._func(from_account, gas, gas_price_value, nonce)
                    tx_hashes.append(tx_hash)

                    # If this is the first transaction sent, get its nonce so we can override the transaction with
                    # another one using higher gas price if :py:class:`pymaker.gas.GasPrice` tells us to do so
                    if nonce is None:
                        nonce = self.web3.eth.getTransaction(tx_hash)['nonce']

                    self.logger.info(f"Sent transaction {self.name()} with nonce={nonce}, gas={gas},"
                                     f" gas_price={gas_price_value if gas_price_value is not None else 'default'}"
                                     f" (tx_hash={tx_hash})")
                except:
                    self.logger.warning(f"Failed to send transaction {self.name()} with nonce={nonce}, gas={gas},"
                                        f" gas_price={gas_price_value if gas_price_value is not None else 'default'}")

                    if len(tx_hashes) == 0:
                        raise

            await asyncio.sleep(0.25)
예제 #17
0
    def __init__(self, args: list, **kwargs):
        """Pass in arguements assign necessary variables/objects and instantiate other Classes"""

        parser = argparse.ArgumentParser("chief-keeper")

        parser.add_argument(
            "--rpc-host",
            type=str,
            default="https://localhost:8545",
            help="JSON-RPC host:port (default: 'localhost:8545')")

        parser.add_argument("--rpc-timeout",
                            type=int,
                            default=10,
                            help="JSON-RPC timeout (in seconds, default: 10)")

        parser.add_argument(
            "--network",
            type=str,
            required=True,
            help=
            "Network that you're running the Keeper on (options, 'mainnet', 'kovan', 'testnet')"
        )

        parser.add_argument(
            "--eth-from",
            type=str,
            required=True,
            help=
            "Ethereum address from which to send transactions; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument(
            "--eth-key",
            type=str,
            nargs='*',
            help=
            "Ethereum private key(s) to use (e.g. 'key_file=/path/to/keystore.json,pass_file=/path/to/passphrase.txt')"
        )

        parser.add_argument(
            "--dss-deployment-file",
            type=str,
            required=False,
            help=
            "Json description of all the system addresses (e.g. /Full/Path/To/configFile.json)"
        )

        parser.add_argument(
            "--chief-deployment-block",
            type=int,
            required=False,
            default=0,
            help=
            " Block that the Chief from dss-deployment-file was deployed at (e.g. 8836668"
        )

        parser.add_argument(
            "--max-errors",
            type=int,
            default=100,
            help=
            "Maximum number of allowed errors before the keeper terminates (default: 100)"
        )

        parser.add_argument("--debug",
                            dest='debug',
                            action='store_true',
                            help="Enable debug output")

        parser.add_argument("--ethgasstation-api-key",
                            type=str,
                            default=None,
                            help="ethgasstation API key")
        parser.add_argument("--gas-initial-multiplier",
                            type=str,
                            default=1.0,
                            help="ethgasstation API key")
        parser.add_argument("--gas-reactive-multiplier",
                            type=str,
                            default=2.25,
                            help="gas strategy tuning")
        parser.add_argument("--gas-maximum",
                            type=str,
                            default=5000,
                            help="gas strategy tuning")

        parser.set_defaults(cageFacilitated=False)
        self.arguments = parser.parse_args(args)

        self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else web3_via_http(
            endpoint_uri=self.arguments.rpc_host,
            timeout=self.arguments.rpc_timeout,
            http_pool_size=100)

        self.web3.eth.defaultAccount = self.arguments.eth_from
        register_keys(self.web3, self.arguments.eth_key)
        self.our_address = Address(self.arguments.eth_from)

        if self.arguments.dss_deployment_file:
            self.dss = DssDeployment.from_json(
                web3=self.web3,
                conf=open(self.arguments.dss_deployment_file, "r").read())
        else:
            self.dss = DssDeployment.from_network(
                web3=self.web3, network=self.arguments.network)

        self.deployment_block = self.arguments.chief_deployment_block

        self.max_errors = self.arguments.max_errors
        self.errors = 0

        self.confirmations = 0

        # Create dynamic gas strategy
        if self.arguments.ethgasstation_api_key:
            self.gas_price = DynamicGasPrice(self.arguments, self.web3)
        else:
            self.gas_price = DefaultGasPrice()

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=(logging.DEBUG if self.arguments.debug else logging.INFO))
예제 #18
0
    async def transact_async(self, **kwargs) -> Optional[Receipt]:
        """Executes the Ethereum transaction asynchronously.

        Executes the Ethereum transaction asynchronously. The method will return immediately.
        Ultimately, its future value will become either a :py:class:`pymaker.Receipt` or `None`,
        depending on whether the transaction execution was successful or not.

        Out-of-gas exceptions are automatically recognized as transaction failures.

        Allowed keyword arguments are: `from_address`, `replace`, `gas`, `gas_buffer`, `gas_price`.
        `gas_price` needs to be an instance of a class inheriting from :py:class:`pymaker.gas.GasPrice`.

        The `gas` keyword argument is the gas limit for the transaction, whereas `gas_buffer`
        specifies how much gas should be added to the estimate. They can not be present
        at the same time. If none of them are present, a default buffer is added to the estimate.

        Returns:
            A future value of either a :py:class:`pymaker.Receipt` object if the transaction
            invocation was successful, or `None` if it failed.
        """

        self.initial_time = time.time()
        unknown_kwargs = set(kwargs.keys()) - {
            'from_address', 'replace', 'gas', 'gas_buffer', 'gas_price'
        }
        if len(unknown_kwargs) > 0:
            raise Exception(f"Unknown kwargs: {unknown_kwargs}")

        # Get the from account.
        from_account = kwargs['from_address'].address if (
            'from_address' in kwargs) else self.web3.eth.defaultAccount

        # First we try to estimate the gas usage of the transaction. If gas estimation fails
        # it means there is no point in sending the transaction, thus we fail instantly and
        # do not increment the nonce. If the estimation is successful, we pass the calculated
        # gas value (plus some `gas_buffer`) to the subsequent `transact` calls so it does not
        # try to estimate it again.
        try:
            gas_estimate = self.estimated_gas(Address(from_account))
        except:
            if Transact.gas_estimate_for_bad_txs:
                self.logger.warning(
                    f"Transaction {self.name()} will fail, submitting anyway")
                gas_estimate = Transact.gas_estimate_for_bad_txs
            else:
                self.logger.warning(
                    f"Transaction {self.name()} will fail, refusing to send ({sys.exc_info()[1]})"
                )
                return None

        # Get or calculate `gas`. Get `gas_price`, which in fact refers to a gas pricing algorithm.
        gas = self._gas(gas_estimate, **kwargs)
        self.gas_price = kwargs['gas_price'] if (
            'gas_price' in kwargs) else DefaultGasPrice()
        assert (isinstance(self.gas_price, GasPrice))

        # Get the transaction this one is supposed to replace.
        # If there is one, try to borrow the nonce from it as long as that transaction isn't finished.
        replaced_tx = kwargs['replace'] if ('replace' in kwargs) else None
        if replaced_tx is not None:
            while replaced_tx.nonce is None and replaced_tx.status != TransactStatus.FINISHED:
                await asyncio.sleep(0.25)

            replaced_tx.replaced = True
            self.nonce = replaced_tx.nonce
            # Gas should be calculated from the original time of submission
            self.initial_time = replaced_tx.initial_time if replaced_tx.initial_time else time.time(
            )
            # Use gas strategy from the original transaction if one was not provided
            if 'gas_price' not in kwargs:
                self.gas_price = replaced_tx.gas_price if replaced_tx.gas_price else DefaultGasPrice(
                )
            self.gas_price_last = replaced_tx.gas_price_last
            # Detain replacement until gas strategy produces a price acceptable to the node
            if replaced_tx.tx_hashes:
                most_recent_tx = replaced_tx.tx_hashes[-1]
                self.tx_hashes = [most_recent_tx]

        while True:
            seconds_elapsed = int(time.time() - self.initial_time)

            # CAUTION: if transact_async is called rapidly, we will hammer the node with these JSON-RPC requests
            if self.nonce is not None and self.web3.eth.getTransactionCount(
                    from_account) > self.nonce:
                # Check if any transaction sent so far has been mined (has a receipt).
                # If it has, we return either the receipt (if if was successful) or `None`.
                for attempt in range(1, 11):
                    if self.replaced:
                        self.logger.info(
                            f"Transaction with nonce={self.nonce} was replaced with a newer transaction"
                        )
                        return None

                    for tx_hash in self.tx_hashes:
                        receipt = self._get_receipt(tx_hash)
                        if receipt:
                            if receipt.successful:
                                self.logger.info(
                                    f"Transaction {self.name()} was successful (tx_hash={tx_hash})"
                                )
                                return receipt
                            else:
                                self.logger.warning(
                                    f"Transaction {self.name()} mined successfully but generated no single"
                                    f" log entry, assuming it has failed (tx_hash={tx_hash})"
                                )
                                return None

                    self.logger.debug(
                        f"No receipt found in attempt #{attempt}/10 (nonce={self.nonce},"
                        f" getTransactionCount={self.web3.eth.getTransactionCount(from_account)})"
                    )

                    await asyncio.sleep(0.5)

                # If we can not find a mined receipt but at the same time we know last used nonce
                # has increased, then it means that the transaction we tried to send failed.
                self.logger.warning(
                    f"Transaction {self.name()} has been overridden by another transaction"
                    f" with the same nonce, which means it has failed")
                return None

            # Trap replacement after the tx has entered the mempool and before it has been mined
            if self.replaced:
                self.logger.info(
                    f"Transaction {self.name()} with nonce={self.nonce} is being replaced"
                )
                return None

            # Send a transaction if:
            # - no transaction has been sent yet, or
            # - the requested gas price has changed enough since the last transaction has been sent
            # - the gas price on a replacement has sufficiently exceeded that of the original transaction
            gas_price_value = self.gas_price.get_gas_price(seconds_elapsed)
            transaction_was_sent = len(self.tx_hashes) > 0 or (
                replaced_tx is not None and len(replaced_tx.tx_hashes) > 0)
            # Uncomment this to debug state during transaction submission
            # self.logger.debug(f"Transaction {self.name()} is churning: was_sent={transaction_was_sent}, gas_price_value={gas_price_value} gas_price_last={self.gas_price_last}")
            if not transaction_was_sent or (
                    gas_price_value is not None
                    and gas_price_value > self.gas_price_last * 1.125):
                self.gas_price_last = gas_price_value

                try:
                    # We need the lock in order to not try to send two transactions with the same nonce.
                    with transaction_lock:
                        if self.nonce is None:
                            if _is_parity(self.web3):
                                self.nonce = int(
                                    self.web3.manager.request_blocking(
                                        "parity_nextNonce", [from_account]),
                                    16)
                            else:
                                self.nonce = self.web3.eth.getTransactionCount(
                                    from_account, block_identifier='pending')

                        # Trap replacement while original is holding the lock awaiting nonce assignment
                        if self.replaced:
                            self.logger.info(
                                f"Transaction {self.name()} with nonce={self.nonce} was replaced"
                            )
                            return None

                        tx_hash = self._func(from_account, gas,
                                             gas_price_value, self.nonce)
                        self.tx_hashes.append(tx_hash)

                    self.logger.info(
                        f"Sent transaction {self.name()} with nonce={self.nonce}, gas={gas},"
                        f" gas_price={gas_price_value if gas_price_value is not None else 'default'}"
                        f" (tx_hash={tx_hash})")
                except Exception as e:
                    self.logger.warning(
                        f"Failed to send transaction {self.name()} with nonce={self.nonce}, gas={gas},"
                        f" gas_price={gas_price_value if gas_price_value is not None else 'default'}"
                        f" ({e})")

                    if len(self.tx_hashes) == 0:
                        raise

            await asyncio.sleep(0.25)
예제 #19
0
 def gas_price(self):
     """  DefaultGasPrice """
     return DefaultGasPrice()
 def gas_price(self):
     if self.arguments.gas_price > 0:
         return FixedGasPrice(self.arguments.gas_price)
     else:
         return DefaultGasPrice()
예제 #21
0
    def __init__(self, args: list, **kwargs):
        parser = argparse.ArgumentParser(prog='auction-keeper')

        parser.add_argument("--rpc-host",
                            type=str,
                            default="localhost",
                            help="JSON-RPC host (default: `localhost')")
        parser.add_argument("--rpc-port",
                            type=int,
                            default=8545,
                            help="JSON-RPC port (default: `8545')")
        parser.add_argument("--rpc-timeout",
                            type=int,
                            default=10,
                            help="JSON-RPC timeout (in seconds, default: 10)")

        parser.add_argument(
            "--eth-from",
            type=str,
            required=True,
            help="Ethereum account from which to send transactions")
        parser.add_argument(
            "--eth-key",
            type=str,
            nargs='*',
            help=
            "Ethereum private key(s) to use (e.g. 'key_file=aaa.json,pass_file=aaa.pass')"
        )

        parser.add_argument('--type',
                            type=str,
                            choices=['flip', 'flap', 'flop'],
                            help="Auction type in which to participate")
        parser.add_argument(
            '--ilk',
            type=str,
            help=
            "Name of the collateral type for a flip keeper (e.g. 'ETH-B', 'ZRX-A'); "
            "available collateral types can be found at the left side of the CDP Portal"
        )

        parser.add_argument(
            '--bid-only',
            dest='create_auctions',
            action='store_false',
            help="Do not take opportunities to create new auctions")
        parser.add_argument('--min-auction',
                            type=int,
                            default=1,
                            help="Lowest auction id to consider")
        parser.add_argument(
            '--max-auctions',
            type=int,
            default=1000,
            help="Maximum number of auctions to simultaneously interact with, "
            "used to manage OS and hardware limitations")
        parser.add_argument(
            '--min-flip-lot',
            type=float,
            default=0,
            help="Minimum lot size to create or bid upon a flip auction")
        parser.add_argument(
            '--bid-delay',
            type=float,
            default=0.0,
            help=
            "Seconds to wait between bids, used to manage OS and hardware limitations"
        )
        parser.add_argument(
            '--shard-id',
            type=int,
            default=0,
            help=
            "When sharding auctions across multiple keepers, this identifies the shard"
        )
        parser.add_argument(
            '--shards',
            type=int,
            default=1,
            help=
            "Number of shards; should be one greater than your highest --shard-id"
        )

        parser.add_argument(
            "--vulcanize-endpoint",
            type=str,
            help=
            "When specified, frob history will be queried from a VulcanizeDB lite node, "
            "reducing load on the Ethereum node for flip auctions")
        parser.add_argument(
            '--from-block',
            type=int,
            help=
            "Starting block from which to look at history (set to block where MCD was deployed)"
        )

        parser.add_argument(
            '--vat-dai-target',
            type=float,
            help="Amount of Dai to keep in the Vat contract (e.g. 2000)")
        parser.add_argument(
            '--keep-dai-in-vat-on-exit',
            dest='exit_dai_on_shutdown',
            action='store_false',
            help=
            "Retain Dai in the Vat on exit, saving gas when restarting the keeper"
        )
        parser.add_argument('--keep-gem-in-vat-on-exit',
                            dest='exit_gem_on_shutdown',
                            action='store_false',
                            help="Retain collateral in the Vat on exit")

        parser.add_argument(
            "--model",
            type=str,
            required=True,
            nargs='+',
            help="Commandline to use in order to start the bidding model")
        parser.add_argument("--ethgasstation-api-key",
                            type=str,
                            default=None,
                            help="ethgasstation API key")

        parser.add_argument("--debug",
                            dest='debug',
                            action='store_true',
                            help="Enable debug output")

        self.arguments = parser.parse_args(args)

        # Configure connection to the chain
        if self.arguments.rpc_host.startswith("http"):
            endpoint_uri = f"{self.arguments.rpc_host}:{self.arguments.rpc_port}"
        else:
            # Should probably default this to use TLS, but I don't want to break existing configs
            endpoint_uri = f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}"
        self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=endpoint_uri,
                request_kwargs={"timeout": self.arguments.rpc_timeout}))
        self.web3.eth.defaultAccount = self.arguments.eth_from
        register_keys(self.web3, self.arguments.eth_key)
        self.our_address = Address(self.arguments.eth_from)

        # Check configuration for retrieving urns/bites
        if self.arguments.type == 'flip' and self.arguments.create_auctions \
                and self.arguments.from_block is None and self.arguments.vulcanize_endpoint is None:
            raise RuntimeError(
                "Either --from-block or --vulcanize-endpoint must be specified to kick off "
                "flip auctions")
        if self.arguments.type == 'flip' and not self.arguments.ilk:
            raise RuntimeError(
                "--ilk must be supplied when configuring a flip keeper")
        if self.arguments.type == 'flop' and self.arguments.create_auctions \
                and self.arguments.from_block is None:
            raise RuntimeError(
                "--from-block must be specified to kick off flop auctions")

        # Configure core and token contracts
        mcd = DssDeployment.from_node(web3=self.web3)
        self.vat = mcd.vat
        self.cat = mcd.cat
        self.vow = mcd.vow
        self.mkr = mcd.mkr
        self.dai_join = mcd.dai_adapter
        if self.arguments.type == 'flip':
            self.collateral = mcd.collaterals[self.arguments.ilk]
            self.ilk = self.collateral.ilk
            self.gem_join = self.collateral.adapter
        else:
            self.collateral = None
            self.ilk = None
            self.gem_join = None

        # Configure auction contracts
        self.flipper = self.collateral.flipper if self.arguments.type == 'flip' else None
        self.flapper = mcd.flapper if self.arguments.type == 'flap' else None
        self.flopper = mcd.flopper if self.arguments.type == 'flop' else None
        self.urn_history = None
        if self.flipper:
            self.min_flip_lot = Wad.from_number(self.arguments.min_flip_lot)
            self.strategy = FlipperStrategy(self.flipper, self.min_flip_lot)
            self.urn_history = UrnHistory(self.web3, mcd, self.ilk,
                                          self.arguments.from_block,
                                          self.arguments.vulcanize_endpoint)
        elif self.flapper:
            self.strategy = FlapperStrategy(self.flapper, self.mkr.address)
        elif self.flopper:
            self.strategy = FlopperStrategy(self.flopper)
        else:
            raise RuntimeError("Please specify auction type")

        # Create the collection used to manage auctions relevant to this keeper
        self.auctions = Auctions(
            flipper=self.flipper.address if self.flipper else None,
            flapper=self.flapper.address if self.flapper else None,
            flopper=self.flopper.address if self.flopper else None,
            model_factory=ModelFactory(' '.join(self.arguments.model)))
        self.auctions_lock = threading.Lock()
        self.dead_auctions = set()
        self.lifecycle = None

        # Create gas strategy used for non-bids
        if self.arguments.ethgasstation_api_key:
            self.gas_price = DynamicGasPrice(
                self.arguments.ethgasstation_api_key)
        else:
            self.gas_price = DefaultGasPrice()

        self.vat_dai_target = Wad.from_number(self.arguments.vat_dai_target) if \
            self.arguments.vat_dai_target is not None else None

        logging.basicConfig(
            format='%(asctime)-15s %(levelname)-8s %(message)s',
            level=(logging.DEBUG if self.arguments.debug else logging.INFO))
        # reduce logspew
        logging.getLogger('urllib3').setLevel(logging.INFO)
        logging.getLogger("web3").setLevel(logging.INFO)
        logging.getLogger("asyncio").setLevel(logging.INFO)
        logging.getLogger("requests").setLevel(logging.INFO)