def _mine(config, client, wallet, dashboard=False): """ Start a mining chip if not already running. Otherwise mine at CLI. On a 21 Bitcoin Computer, we attempt to start the mining chip if is not already running. If it is already running, repeated invocation of 21 mine will result in buffered mining (advances against the next day's mining proceeds). Finally, if we are running the 21 software on an arbitrary device (i.e. not on a Bitcoin Computer), we prompt the user to use 21 earn instead. Args: config (Config): config object used for getting .two1 information client (two1.server.rest_client.TwentyOneRestClient) an object for sending authenticated requests to the TwentyOne backend. wallet (two1.wallet.Wallet): a user's wallet instance dashboard (bool): shows minertop dashboard if True """ if bitcoin_computer.has_mining_chip(): if not is_minerd_running(): start_minerd(config, dashboard) elif dashboard: show_minertop(dashboard) # if minerd is running and we have not specified a dashboard # flag do a cpu mine else: start_cpu_mining(config.username, client, wallet) else: logger.info(uxstring.UxString.use_21_earn_instead) sys.exit(1)
def status_mining(client): """ Prints the mining status if the device has a mining chip Args: client (TwentyOneRestClient): rest client used for communication with the backend api Returns: dict: a dictionary containing 'is_mining', 'hashrate', and 'mined' values """ has_chip = bitcoin_computer.has_mining_chip() is_mining, mined, hashrate = None, None, None if has_chip: try: hashrate = bitcoin_computer.get_hashrate("15min") if hashrate > 0: hashrate = uxstring.UxString.status_mining_hashrate.format( hashrate / 1e9) else: hashrate = uxstring.UxString.status_mining_hashrate_unknown except FileNotFoundError: is_mining = uxstring.UxString.status_mining_file_not_found except TimeoutError: is_mining = uxstring.UxString.status_mining_timeout else: is_mining = uxstring.UxString.status_mining_success mined = client.get_mined_satoshis() logger.info( uxstring.UxString.status_mining.format(is_mining, hashrate, mined)) return dict(is_mining=is_mining, hashrate=hashrate, mined=mined)
def status_mining(client): """ Prints the mining status if the device has a mining chip Args: client (TwentyOneRestClient): rest client used for communication with the backend api Returns: dict: a dictionary containing 'is_mining', 'hashrate', and 'mined' values """ has_chip = bitcoin_computer.has_mining_chip() is_mining, mined, hashrate = None, None, None if has_chip: try: hashrate = bitcoin_computer.get_hashrate("15min") if hashrate > 0: hashrate = uxstring.UxString.status_mining_hashrate.format(hashrate/1e9) else: hashrate = uxstring.UxString.status_mining_hashrate_unknown except FileNotFoundError: is_mining = uxstring.UxString.status_mining_file_not_found except TimeoutError: is_mining = uxstring.UxString.status_mining_timeout else: is_mining = uxstring.UxString.status_mining_success mined = client.get_mined_satoshis() logger.info(uxstring.UxString.status_mining.format(is_mining, hashrate, mined)) return dict(is_mining=is_mining, hashrate=hashrate, mined=mined)
def _mine(config, client, wallet, dashboard=False): """ Start a mining chip if not already running. Otherwise mine at CLI. On a 21 Bitcoin Computer, we attempt to start the mining chip if is not already running. If it is already running, repeated invocation of 21 mine will result in buffered mining (advances against the next day's mining proceeds). Finally, if we are running the 21 software on an arbitrary device (i.e. not on a Bitcoin Computer), we prompt the user to use 21 earn instead. Args: config (Config): config object used for getting .two1 information client (two1.server.rest_client.TwentyOneRestClient) an object for sending authenticated requests to the TwentyOne backend. wallet (two1.wallet.Wallet): a user's wallet instance dashboard (bool): shows minertop dashboard if True """ if bitcoin_computer.has_mining_chip(): if not is_minerd_running(): start_minerd(config, dashboard) elif dashboard: show_minertop(dashboard) # if minerd is running and we have not specified a dashboard # flag do a cpu mine else: start_cpu_mining(config.username, client, wallet) else: logger.info(uxstring.UxString.use_21_earn_instead) sys.exit(1)
def _send(wallet, address, satoshis, verbose, use_unconfirmed=False): """Send bitcoin to the specified address""" txids = [] try: txids = wallet.send_to(address=address, amount=satoshis, use_unconfirmed=use_unconfirmed) # For now there is only a single txn created, so assume it's 0 txid, txn = txids[0]["txid"], txids[0]["txn"] if verbose: logger.info(uxstring.UxString.send_success_verbose.format(satoshis, address, txid, txn)) else: logger.info(uxstring.UxString.send_success.format(satoshis, address, txid)) except ValueError as e: # This will trigger if there's a below dust-limit output. raise exceptions.Two1Error(str(e)) except WalletBalanceError as e: if wallet.unconfirmed_balance() > satoshis: raise exceptions.Two1Error(uxstring.UxString.send_insufficient_confirmed + str(e)) else: balance = min(wallet.confirmed_balance(), wallet.unconfirmed_balance()) if has_mining_chip(): raise exceptions.Two1Error(uxstring.UxString.send_insufficient_blockchain_21bc.format( balance, satoshis, address, str(e))) else: raise exceptions.Two1Error(uxstring.UxString.send_insufficient_blockchain_free.format( balance, satoshis, address, str(e))) except DataProviderError as e: if "rejected" in str(e): raise exceptions.Two1Error(uxstring.UxString.send_rejected) else: raise exceptions.Two1Error(str(e)) return txids
def get_work(client): """ Get work from the pool using the rest client. Args: client (TwentyOneRestClient): rest client used for communication with the backend api Returns: WorkNotification: a Swirl work notification message """ try: response = client.get_work() except exceptions.ServerRequestError as e: if e.status_code == 403 and "detail" in e.data and "TO200" in e.data["detail"]: raise exceptions.BitcoinComputerNeededError( msg=uxstring.UxString.mining_bitcoin_computer_needed, response=response) elif e.status_code == 403 and e.data.get("detail") == "TO201": raise exceptions.MiningDisabledError(uxstring.UxString.Error.suspended_account) elif e.status_code == 403 and e.data.get("detail") == "TO501": raise exceptions.MiningDisabledError(uxstring.UxString.daily_mining_limit_reached) elif e.status_code == 403 and e.data.get("detail") == "TO502": raise exceptions.MiningDisabledError(uxstring.UxString.lifetime_earn_limit_reached) elif e.status_code == 404: if has_mining_chip(): raise exceptions.MiningDisabledError(uxstring.UxString.daily_mining_limit_reached) else: raise exceptions.MiningDisabledError(uxstring.UxString.earn_limit_reached) else: raise e msg_factory = message_factory.SwirlMessageFactory() msg = base64.decodebytes(response.content) work = msg_factory.read_object(msg) return work
def check_BC_has_chip(self): """ Checks if the system has a 21 bitcoin shield Returns: Check.Result, str, str: Result of the check Human readable message describing the check "Yes" if the device has a bitcoin shield, "No" otherwise """ check_str = "Has Mining Chip" if bitcoin_computer.has_mining_chip(): return Check.Result.PASS, check_str, "Yes" else: return Check.Result.FAIL, check_str, "No"
def check_BC_has_chip(self): """ Checks if the system has a 21 bitcoin shield Returns: Check.Result, str, str: Result of the check Human readable message describing the check "Yes" if the device has a bitcoin shield, "No" otherwise """ check_str = "Has Mining Chip" if bitcoin_computer.has_mining_chip(): return Check.Result.PASS, check_str, "Yes" else: return Check.Result.FAIL, check_str, "No"
def login_account(config, machine_auth, username=None, password=None): """ Log in a user into the two1 account Args: config (Config): config object used for getting .two1 information username (str): optional command line arg to skip username prompt password (str): optional command line are to skip password prompt """ # prints the sign up page link when a username is not set and not on a BC if not config.username and not bitcoin_computer.has_mining_chip(): logger.info(uxstring.UxString.signin_title) # uses specifies username or asks for a different one username = username or get_username_interactive() password = password or get_password_interactive() # use existing username in config rest_client = _rest_client.TwentyOneRestClient(two1.TWO1_HOST, machine_auth, username) # get the payout address and the pubkey from the machine auth wallet machine_auth_pubkey_b64 = base64.b64encode( machine_auth.public_key.compressed_bytes).decode() payout_address = machine_auth.wallet.current_address logger.info(uxstring.UxString.login_in_progress.format(username)) try: rest_client.login(payout_address=payout_address, password=password) # handles 401 gracefully except exceptions.ServerRequestError as ex: if ex.status_code == 403 and "error" in ex.data and ex.data[ "error"] == "TO408": email = ex.data["email"] raise exceptions.UnloggedException( click.style(uxstring.UxString.unconfirmed_email.format(email), fg="blue")) elif ex.status_code == 403 or ex.status_code == 404: raise exceptions.UnloggedException( uxstring.UxString.incorrect_password) else: raise ex logger.info(uxstring.UxString.payout_address.format(payout_address)) logger.info(uxstring.UxString.get_started) # Save the new username and auth key config.set("username", username) config.set("mining_auth_pubkey", machine_auth_pubkey_b64) config.save()
def login_account(config, machine_auth, username=None, password=None): """ Log in a user into the two1 account Args: config (Config): config object used for getting .two1 information username (str): optional command line arg to skip username prompt password (str): optional command line are to skip password prompt """ # prints the sign up page link when a username is not set and not on a BC if not config.username and not bitcoin_computer.has_mining_chip(): logger.info(uxstring.UxString.signin_title) # uses specifies username or asks for a different one username = username or get_username_interactive() password = password or get_password_interactive() # use existing username in config rest_client = _rest_client.TwentyOneRestClient(two1.TWO1_HOST, machine_auth, username) # get the payout address and the pubkey from the machine auth wallet machine_auth_pubkey_b64 = base64.b64encode(machine_auth.public_key.compressed_bytes).decode() payout_address = machine_auth.wallet.current_address logger.info(uxstring.UxString.login_in_progress.format(username)) try: rest_client.login(payout_address=payout_address, password=password) # handles 401 gracefully except exceptions.ServerRequestError as ex: if ex.status_code == 403 and "error" in ex.data and ex.data["error"] == "TO408": email = ex.data["email"] raise exceptions.UnloggedException( click.style(uxstring.UxString.unconfirmed_email.format(email), fg="blue")) elif ex.status_code == 403 or ex.status_code == 404: raise exceptions.UnloggedException(uxstring.UxString.incorrect_password) else: raise ex logger.info(uxstring.UxString.payout_address.format(payout_address)) logger.info(uxstring.UxString.get_started) # If config file hasn't been created yet ask for opt-in to analytics if not config.username: analytics_optin(config) # Save the new username and auth key config.set("username", username) config.set("mining_auth_pubkey", machine_auth_pubkey_b64) config.save()
def get_work(client): """ Get work from the pool using the rest client. Args: client (TwentyOneRestClient): rest client used for communication with the backend api Returns: WorkNotification: a Swirl work notification message """ try: response = client.get_work() except exceptions.ServerRequestError as e: if e.status_code == 403 and "detail" in e.data and "TO200" in e.data[ "detail"]: raise exceptions.BitcoinComputerNeededError( msg=uxstring.UxString.mining_bitcoin_computer_needed, response=response) elif e.status_code == 403 and e.data.get("detail") == "TO201": raise exceptions.MiningDisabledError( uxstring.UxString.Error.suspended_account) elif e.status_code == 403 and e.data.get("detail") == "TO501": raise exceptions.MiningDisabledError( uxstring.UxString.monthly_mining_limit_reached) elif e.status_code == 403 and e.data.get("detail") == "TO502": raise exceptions.MiningDisabledError( uxstring.UxString.lifetime_earn_limit_reached) elif e.status_code == 403 and e.data.get("detail") == "TO503": raise exceptions.MiningDisabledError( uxstring.UxString.no_earn_allocations.format( two1.TWO1_WWW_HOST, client.username)) elif e.status_code == 404: if bitcoin_computer.has_mining_chip(): raise exceptions.MiningDisabledError( uxstring.UxString.monthly_mining_limit_reached) else: raise exceptions.MiningDisabledError( uxstring.UxString.earn_limit_reached) else: raise e msg_factory = message_factory.SwirlMessageFactory() msg = base64.decodebytes(response.content) work = msg_factory.read_object(msg) return work
def get_work(client): """ Get work from the pool using the rest client. Args: client (TwentyOneRestClient): rest client used for communication with the backend api Returns: WorkNotification: a Swirl work notification message """ try: response = client.get_work() except exceptions.ServerRequestError as e: profile_url = "{}/{}".format(two1.TWO1_WWW_HOST, client.username) profile_cta = uxstring.UxString.mining_profile_call_to_action.format( profile_url) err_string = None if e.status_code == 403 and e.data.get("error") == "TO201": err_string = uxstring.UxString.Error.suspended_account elif e.status_code == 403 and e.data.get("error") == "TO501": err_string = uxstring.UxString.monthly_mining_limit_reached elif e.status_code == 403 and e.data.get("error") == "TO502": err_string = uxstring.UxString.lifetime_earn_limit_reached elif e.status_code == 403 and e.data.get("error") == "TO503": err_string = uxstring.UxString.no_earn_allocations.format( two1.TWO1_WWW_HOST, client.username) elif e.status_code == 404: if bitcoin_computer.has_mining_chip(): err_string = uxstring.UxString.monthly_mining_limit_reached else: err_string = uxstring.UxString.earn_limit_reached if err_string: raise exceptions.MiningDisabledError("{}\n\n{}".format( err_string, profile_cta)) else: raise e msg_factory = message_factory.SwirlMessageFactory() msg = base64.decodebytes(response.content) work = msg_factory.read_object(msg) return work
def _send(wallet, address, satoshis, verbose, use_unconfirmed=False): """Send bitcoin to the specified address""" txids = [] try: txids = wallet.send_to(address=address, amount=satoshis, use_unconfirmed=use_unconfirmed) # For now there is only a single txn created, so assume it's 0 txid, txn = txids[0]["txid"], txids[0]["txn"] if verbose: logger.info( uxstring.UxString.send_success_verbose.format( satoshis, address, txid, txn)) else: logger.info( uxstring.UxString.send_success.format(satoshis, address, txid)) except ValueError as e: # This will trigger if there's a below dust-limit output. raise exceptions.Two1Error(str(e)) except WalletBalanceError as e: if wallet.unconfirmed_balance() > satoshis: raise exceptions.Two1Error( uxstring.UxString.send_insufficient_confirmed + str(e)) else: balance = min(wallet.confirmed_balance(), wallet.unconfirmed_balance()) if has_mining_chip(): raise exceptions.Two1Error( uxstring.UxString.send_insufficient_blockchain_21bc.format( balance, satoshis, address)) else: raise exceptions.Two1Error( uxstring.UxString.send_insufficient_blockchain_free.format( balance, satoshis, address)) except DataProviderError as e: if "rejected" in str(e): raise exceptions.Two1Error(uxstring.UxString.send_rejected) else: raise exceptions.Two1Error(str(e)) return txids
def get_work(client): """ Get work from the pool using the rest client. Args: client (TwentyOneRestClient): rest client used for communication with the backend api Returns: WorkNotification: a Swirl work notification message """ try: response = client.get_work() except exceptions.ServerRequestError as e: profile_url = "{}/{}".format(two1.TWO1_WWW_HOST, client.username) profile_cta = uxstring.UxString.mining_profile_call_to_action.format(profile_url) err_string = None if e.status_code == 403 and e.data.get("error") == "TO201": err_string = uxstring.UxString.Error.suspended_account elif e.status_code == 403 and e.data.get("error") == "TO501": err_string = uxstring.UxString.monthly_mining_limit_reached elif e.status_code == 403 and e.data.get("error") == "TO502": err_string = uxstring.UxString.lifetime_earn_limit_reached elif e.status_code == 403 and e.data.get("error") == "TO503": err_string = uxstring.UxString.no_earn_allocations.format(two1.TWO1_WWW_HOST, client.username) elif e.status_code == 404: if bitcoin_computer.has_mining_chip(): err_string = uxstring.UxString.monthly_mining_limit_reached else: err_string = uxstring.UxString.earn_limit_reached if err_string: raise exceptions.MiningDisabledError("{}\n\n{}".format(err_string, profile_cta)) else: raise e msg_factory = message_factory.SwirlMessageFactory() msg = base64.decodebytes(response.content) work = msg_factory.read_object(msg) return work
def _buy(config, client, machine_auth, resource, info_only=False, payment_method='offchain', header=(), method='GET', output_file=None, data=None, data_file=None, maxprice=10000): """Purchase a 402-enabled resource via CLI. This function attempts to purchase the requested resource using the `payment_method` and then write out its results to STDOUT. This allows a user to view results or pipe them into another command-line function. Args: config (two1.commands.config.Config): an object necessary for various user-specific actions, as well as for using the `capture_usage` function decorator. client (two1.server.rest_client.TwentyOneRestClient) an object for sending authenticated requests to the TwentyOne backend. machine_auth (two1.server.machine_auth_wallet.MachineAuthWallet): a wallet used for machine authentication. resource (str): a URI of the form scheme://host:port/path with `http` and `https` strictly enforced as required schemes. info_only (bool): if True, do not purchase the resource, and cause the function to write only the 402-related headers. payment_method (str): the payment method used for the purchase. header (tuple): list of HTTP headers to send with the request. method (str): the HTTP method/verb to make with the request. output_file (str): name of the file to redirect function output. data (str): serialized data to send with the request. The function will attempt to deserialize the data and determine its encoding type. data_file (str): name of the data file to send in HTTP body. maxprice (int): allowed maximum price (in satoshis) of the resource. Raises: click.ClickException: if some set of parameters or behavior cause the purchase to not complete successfully for any reason. """ # Find the correct payment method if payment_method == 'offchain': requests = bitrequests.BitTransferRequests(machine_auth, config.username) elif payment_method == 'onchain': requests = bitrequests.OnChainRequests(machine_auth.wallet) elif payment_method == 'channel': requests = bitrequests.ChannelRequests(machine_auth.wallet) else: raise click.ClickException(uxstring.UxString.buy_bad_payment_method.format(payment_method)) # Request user consent if they're creating a channel for the first time if payment_method == 'channel' and not requests._channelclient.list(): confirmed = click.confirm(uxstring.UxString.buy_channel_warning.format( requests.DEFAULT_DEPOSIT_AMOUNT, statemachine.PaymentChannelStateMachine.PAYMENT_TX_MIN_OUTPUT_AMOUNT ), default=True) if not confirmed: raise click.ClickException(uxstring.UxString.buy_channel_aborted) resource = parse_resource(resource) # Retrieve 402-related header information, print it, then exit if info_only: response = requests.get_402_info(resource) return logger.info('\n'.join(['{}: {}'.format(key, val) for key, val in response.items()])) # Collect HTTP header parameters into a single dictionary headers = {key.strip(): value.strip() for key, value in (h.split(':') for h in header)} # Handle data if applicable if data or data_file: method = 'POST' if method == 'GET' else method data, headers['Content-Type'] = _parse_post_data(data) # Make the paid request for the resource try: response = requests.request( method.lower(), resource, max_price=maxprice, data=data or data_file, headers=headers ) except bitrequests.ResourcePriceGreaterThanMaxPriceError as e: raise click.ClickException(uxstring.UxString.Error.resource_price_greater_than_max_price.format(e)) except wallet_exceptions.DustLimitError as e: raise click.ClickException(e) except ValueError as e: if bitcoin_computer.has_mining_chip(): raise click.ClickException(uxstring.UxString.Error.insufficient_funds_mine_more) else: raise click.ClickException(uxstring.UxString.Error.insufficient_funds_earn_more) except Exception as e: raise click.ClickException(e) # Write response text to stdout or a filename if provided if not output_file: try: json_resp = response.json() except ValueError: logger.info(response.content, nl=False) else: if isinstance(json_resp, dict): ordered = OrderedDict(sorted(json_resp.items())) logger.info(json.dumps(ordered, indent=4), nl=False) else: logger.info(json.dumps(json_resp, indent=4), nl=False) else: with open(output_file, 'wb') as f: logger.info(response.content, file=f, nl=False) logger.info('', err=True) # newline for pretty-printing errors to stdout # Exit successfully if no amount was paid for the resource (standard HTTP request) if not hasattr(response, 'amount_paid'): return # Fetch and write out diagnostic payment information for balances if payment_method == 'offchain': twentyone_balance = client.get_earnings()["total_earnings"] logger.info(uxstring.UxString.buy_balances.format(response.amount_paid, '21.co', twentyone_balance), err=True) elif payment_method == 'onchain': onchain_balance = min(requests.wallet.confirmed_balance(), requests.wallet.unconfirmed_balance()) logger.info(uxstring.UxString.buy_balances.format(response.amount_paid, 'blockchain', onchain_balance), err=True) elif payment_method == 'channel': channel_client = requests._channelclient channel_client.sync() channels_balance = sum(s.balance for s in (channel_client.status(url) for url in channel_client.list()) if s.state == channels.PaymentChannelState.READY) logger.info(uxstring.UxString.buy_balances.format(response.amount_paid, 'payment channels', channels_balance), err=True)
def test_has_mining_asic(mock_file, side_effect, outcome): """ Mocks the builtin open function to test various outcomes of opening and reading the product file """ with mock.patch.object(builtins, "open", mock_file) as open_mock: open_mock.side_effect = side_effect assert bitcoin_computer.has_mining_chip() == outcome
def status_wallet(client, wallet, detail=False): """ Logs a formatted string displaying wallet status to the command line Args: client (TwentyOneRestClient): rest client used for communication with the backend api detail (bool): Lists all balance details in status report Returns: dict: a dictionary of 'wallet' and 'buyable' items with formatted strings for each value """ channel_client = channels.PaymentChannelClient(wallet) user_balances = _get_balances(client, wallet, channel_client) status_wallet_dict = { "twentyone_balance": user_balances.twentyone, "onchain": user_balances.onchain, "flushing": user_balances.flushed, "channels_balance": user_balances.channels } logger.info(uxstring.UxString.status_wallet.format(**status_wallet_dict)) if detail: # show balances by address for default wallet address_balances = wallet.balances_by_address(0) status_addresses = [] for addr, balances in address_balances.items(): if balances['confirmed'] > 0 or balances['total'] > 0: status_addresses.append( uxstring.UxString.status_wallet_address.format( addr, balances['confirmed'], balances['total'])) # Display status for all payment channels status_channels = [] for url in channel_client.list(): status_resp = channel_client.status(url) url = urllib.parse.urlparse(url) status_channels.append( uxstring.UxString.status_wallet_channel.format( url.scheme, url.netloc, status_resp.state, status_resp.balance, format_expiration_time(status_resp.expiration_time))) if not len(status_channels): status_channels = [uxstring.UxString.status_wallet_channels_none] logger.info( uxstring.UxString.status_wallet_detail_on.format( addresses=''.join(status_addresses), channels=''.join(status_channels))) else: logger.info(uxstring.UxString.status_wallet_detail_off) total_balance = user_balances.twentyone + user_balances.onchain if total_balance == 0: if bitcoin_computer.has_mining_chip(): command = "21 mine" else: command = "21 earn" logger.info( uxstring.UxString.status_empty_wallet.format( click.style(command, bold=True))) else: buy21 = click.style("21 buy", bold=True) buy21help = click.style("21 buy --help", bold=True) logger.info( uxstring.UxString.status_exit_message.format(buy21, buy21help)) return { "wallet": status_wallet_dict, }
def status_wallet(client, wallet, detail=False): """ Logs a formatted string displaying wallet status to the command line Args: client (TwentyOneRestClient): rest client used for communication with the backend api detail (bool): Lists all balance details in status report Returns: dict: a dictionary of 'wallet' and 'buyable' items with formatted strings for each value """ channel_client = channels.PaymentChannelClient(wallet) user_balances = _get_balances(client, wallet, channel_client) status_wallet_dict = { "twentyone_balance": user_balances.twentyone, "onchain": user_balances.onchain, "flushing": user_balances.flushed, "channels_balance": user_balances.channels } logger.info(uxstring.UxString.status_wallet.format(**status_wallet_dict)) if detail: # show balances by address for default wallet address_balances = wallet.balances_by_address(0) status_addresses = [] for addr, balances in address_balances.items(): if balances['confirmed'] > 0 or balances['total'] > 0: status_addresses.append(uxstring.UxString.status_wallet_address.format( addr, balances['confirmed'], balances['total'])) # Display status for all payment channels status_channels = [] for url in channel_client.list(): status_resp = channel_client.status(url) url = urllib.parse.urlparse(url) status_channels.append(uxstring.UxString.status_wallet_channel.format( url.scheme, url.netloc, status_resp .state, status_resp .balance, format_expiration_time(status_resp .expiration_time))) if not len(status_channels): status_channels = [uxstring.UxString.status_wallet_channels_none] logger.info(uxstring.UxString.status_wallet_detail_on.format( addresses=''.join(status_addresses), channels=''.join(status_channels))) else: logger.info(uxstring.UxString.status_wallet_detail_off) total_balance = user_balances.twentyone + user_balances.onchain if total_balance == 0: if bitcoin_computer.has_mining_chip(): command = "21 mine" else: command = "21 earn" logger.info(uxstring.UxString.status_empty_wallet.format( click.style(command, bold=True))) else: buy21 = click.style("21 buy", bold=True) buy21help = click.style("21 buy --help", bold=True) logger.info(uxstring.UxString.status_exit_message.format(buy21, buy21help)) return { "wallet": status_wallet_dict, }
def _buy(config, client, machine_auth, resource, info_only=False, payment_method='offchain', header=(), method='GET', output_file=None, data=None, data_file=None, maxprice=10000): """Purchase a 402-enabled resource via CLI. This function attempts to purchase the requested resource using the `payment_method` and then write out its results to STDOUT. This allows a user to view results or pipe them into another command-line function. Args: config (two1.commands.config.Config): an object necessary for various user-specific actions, as well as for using the `capture_usage` function decorator. client (two1.server.rest_client.TwentyOneRestClient) an object for sending authenticated requests to the TwentyOne backend. machine_auth (two1.server.machine_auth_wallet.MachineAuthWallet): a wallet used for machine authentication. resource (str): a URI of the form scheme://host:port/path with `http` and `https` strictly enforced as required schemes. info_only (bool): if True, do not purchase the resource, and cause the function to write only the 402-related headers. payment_method (str): the payment method used for the purchase. header (tuple): list of HTTP headers to send with the request. method (str): the HTTP method/verb to make with the request. output_file (str): name of the file to redirect function output. data (str): serialized data to send with the request. The function will attempt to deserialize the data and determine its encoding type. data_file (str): name of the data file to send in HTTP body. maxprice (int): allowed maximum price (in satoshis) of the resource. Raises: click.ClickException: if some set of parameters or behavior cause the purchase to not complete successfully for any reason. """ # Find the correct payment method if payment_method == 'offchain': requests = bitrequests.BitTransferRequests(machine_auth, config.username) elif payment_method == 'onchain': requests = bitrequests.OnChainRequests(machine_auth.wallet) elif payment_method == 'channel': requests = bitrequests.ChannelRequests(machine_auth.wallet) else: raise click.ClickException( uxstring.UxString.buy_bad_payment_method.format(payment_method)) # Request user consent if they're creating a channel for the first time if payment_method == 'channel' and not requests._channelclient.list(): confirmed = click.confirm(uxstring.UxString.buy_channel_warning.format( requests.DEFAULT_DEPOSIT_AMOUNT, statemachine. PaymentChannelStateMachine.PAYMENT_TX_MIN_OUTPUT_AMOUNT), default=True) if not confirmed: raise click.ClickException(uxstring.UxString.buy_channel_aborted) resource = parse_resource(resource) # Retrieve 402-related header information, print it, then exit if info_only: response = requests.get_402_info(resource) return logger.info('\n'.join( ['{}: {}'.format(key, val) for key, val in response.items()])) # Collect HTTP header parameters into a single dictionary headers = { key.strip(): value.strip() for key, value in (h.split(':') for h in header) } # Handle data if applicable if data or data_file: method = 'POST' if method == 'GET' else method data, headers['Content-Type'] = _parse_post_data(data) # Make the paid request for the resource try: response = requests.request(method.lower(), resource, max_price=maxprice, data=data or data_file, headers=headers) except bitrequests.ResourcePriceGreaterThanMaxPriceError as e: raise click.ClickException( uxstring.UxString.Error.resource_price_greater_than_max_price. format(e)) except wallet_exceptions.DustLimitError as e: raise click.ClickException(e) except ValueError as e: if bitcoin_computer.has_mining_chip(): raise click.ClickException( uxstring.UxString.Error.insufficient_funds_mine_more) else: raise click.ClickException( uxstring.UxString.Error.insufficient_funds_earn_more) except Exception as e: raise click.ClickException(e) # Write response text to stdout or a filename if provided if not output_file: try: json_resp = response.json() except ValueError: logger.info(response.content, nl=False) else: if isinstance(json_resp, dict): ordered = OrderedDict(sorted(json_resp.items())) logger.info(json.dumps(ordered, indent=4), nl=False) else: logger.info(json.dumps(json_resp, indent=4), nl=False) else: with open(output_file, 'wb') as f: logger.info(response.content, file=f, nl=False) logger.info('', err=True) # newline for pretty-printing errors to stdout # Exit successfully if no amount was paid for the resource (standard HTTP request) if not hasattr(response, 'amount_paid'): return # Fetch and write out diagnostic payment information for balances if payment_method == 'offchain': twentyone_balance = client.get_earnings()["total_earnings"] logger.info(uxstring.UxString.buy_balances.format( response.amount_paid, '21.co', twentyone_balance), err=True) elif payment_method == 'onchain': onchain_balance = min(requests.wallet.confirmed_balance(), requests.wallet.unconfirmed_balance()) logger.info(uxstring.UxString.buy_balances.format( response.amount_paid, 'blockchain', onchain_balance), err=True) elif payment_method == 'channel': channel_client = requests._channelclient channel_client.sync() channels_balance = sum( s.balance for s in (channel_client.status(url) for url in channel_client.list()) if s.state == channels.PaymentChannelState.READY) logger.info(uxstring.UxString.buy_balances.format( response.amount_paid, 'payment channels', channels_balance), err=True)