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
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()
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()
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()
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))
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)
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()
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)
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)
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
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)
""" 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):
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()
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))
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)
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))
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)
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()
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)