def get_or_create_username(config, machine_auth): """ Gets an existing username or creates a new account On a bitcoin computer a user can create one account per machine auth wallet. When not on a BC a user must log into an existing account created at the free signup page. Args: config (Config): config object used for getting .two1 information machine_auth (MachineAuthWallet): machine auth wallet used for authentication Returns: str: username of the current user on the system """ # User hasn't logged in with the wallet if not config.mining_auth_pubkey: # A user can create an account on a BC if two1.TWO1_DEVICE_ID: login.create_account_on_bc(config, machine_auth) # log into an existing account else: login.login_account(config, machine_auth) if not config.username: raise exceptions.Two1Error(uxstring.UxString.Error.login_error_username) if not config.mining_auth_pubkey: exceptions.Two1Error(uxstring.UxString.Error.login_error_mining_auth_pubkey) return config.username
def _doctor(two1_config): # warm welcome message logger.info(uxstring.UxString.doctor_start) # Get an appointment with the doctor doc = Doctor(two1_config) # Get a general doctor checkup doc.checkup("general") doc.checkup("dependency") doc.checkup("server") if bitcoin_computer.get_device_uuid(): doc.checkup("BC") logger.info("\n" + uxstring.UxString.doctor_total) # groups all checks into one class for reuse of print_summary doc.print_results() if len(doc.get_checks(Check.Result.FAIL)) == 0: return doc.to_dict() else: raise exceptions.Two1Error("21 doctor failed some checks.", json=doc.to_dict())
def buybitcoin_show_info(config, client, exchange): resp = client.get_coinbase_status() if not resp.ok: raise exceptions.Two1Error("Failed to get exchange status") coinbase = resp.json().get("coinbase") if not coinbase: # Not linked, prompt user to info return buybitcoin_config(config, client, exchange) else: payment_method_string = click.style("No Payment Method linked yet.", fg="red", bold=True) if coinbase["payment_method"] is not None: payment_method_string = coinbase["payment_method"]["name"] logger.info(uxstring.UxString.exchange_info_header) logger.info(uxstring.UxString.exchange_info.format(exchange.capitalize(), coinbase["name"], coinbase["account_name"], payment_method_string)) if coinbase["payment_method"] is None: ADD_PAYMENT_METHOD_URL = "https://coinbase.com/quickstarts/payment" logger.info(uxstring.UxString.buybitcoin_no_payment_method.format( exchange.capitalize(), click.style(ADD_PAYMENT_METHOD_URL, fg="blue", bold=True) )) else: logger.info(uxstring.UxString.buybitcoin_instruction_header) logger.info(uxstring.UxString.buybitcoin_instructions.format(exchange.capitalize())) return coinbase
def get_price_quote(client, amount): # first get a quote try: resp = client.buy_bitcoin_from_exchange(amount, "satoshis", commit=False) except exceptions.ServerRequestError as e: if e.status_code == 400: if e.data.get("error") == "TO700": logger.info(uxstring.UxString.minimum_bitcoin_purchase) elif e.data.get("error") == "TO704": logger.info(uxstring.UxString.coinbase_amount_too_high) raise ValueError() elif e.status_code == 403: if e.data.get("error") == "TO703": logger.info(uxstring.UxString.coinbase_max_buy_reached) raise ValueError() buy_result = resp.json() if "err" in buy_result: logger.info(uxstring.UxString.buybitcoin_error.format( click.style(buy_result["err"], bold=True, fg="red"))) raise exceptions.Two1Error("Failed to execute buybitcoin {} {}".format(amount, "satoshis")) fees = buy_result["fees"] total_fees = ["{:.2f} {} {} {}".format( float(fee["amount"]["amount"]), fee["amount"]["currency"], "fee from your" if fee["type"] == "bank" else "fee from", "Coinbase" if fee["type"] == "coinbase" else fee["type"]) for fee in fees] total_fees = click.style(" and ".join(total_fees), bold=True) total_amount = buy_result["total"] total = click.style("{} {}".format(total_amount["amount"], total_amount["currency"]), bold=True) bitcoin_amount = click.style("{} {}".format(int(amount), "satoshis"), bold=True) logger.info(uxstring.UxString.buybitcoin_confirmation.format(total, bitcoin_amount, total, total_fees))
def parse_config( config_file=two1.TWO1_CONFIG_FILE, config_dict=None, need_wallet_and_account=True, check_update=False, debug=False, ): """Get configuration information that is used to drive all 21 commands. This function is very useful for testing as it builds up several key variables (like the client, wallet, username, and the like) that are used in many commands. The way it does this is by taking in the config_file (typically .two1/two1.json) and the config_dict (a list of key-value pairs to override the config_file, typically an empty dictionary), and then running the logic below. It returns obj which is a dictionary that has Config, Wallet, MachineAuth, and TwentyOneRestClient instances underneath it, as well as a string with the username. The obj is passed down by click to various other commands. You can use this function in any test to instantiate the user's wallet, username, and other variables. """ try: config = two1_config.Config(config_file, config_dict, check_update=check_update) except exceptions.FileDecodeError as e: raise click.ClickException( uxstring.UxString.Error.file_decode.format((str(e)))) wallet, machine_auth, username, client = None, None, None, None if need_wallet_and_account: try: wallet = wallet_utils.get_or_create_wallet(config.wallet_path) except blockchain_exceptions.DataProviderError as err: raise exceptions.Two1Error( 'You have experienced a data provider error: %s ' % err.args) machine_auth = machine_auth_wallet.MachineAuthWallet(wallet) username = account_utils.get_or_create_username(config, machine_auth) client = rest_client.TwentyOneRestClient(two1.TWO1_HOST, machine_auth, config.username) config.username = username obj = dict( config=config, wallet=wallet, machine_auth=machine_auth, username=username, client=client, debug=debug, ) return obj
def __init__(self, amount, denomination=SAT, rest_client=None): """Return a new Price object with the provided amount.""" if amount < 0: raise exceptions.Two1Error( 'Parameter `amount` must be a positive number.') if denomination.lower() in Price.SAT: self.denomination = Price.SAT elif denomination.lower() in Price.BTC: if amount < Price.SAT_TO_BTC: raise exceptions.Two1Error( 'Bitcoin amount must be larger than 1e-8') self.denomination = Price.BTC elif denomination.lower() in Price.USD: self.denomination = Price.USD else: raise exceptions.Two1Error( 'Unknown denomination: {}.\nValid denominations: {}.'.format( denomination, Price.DENOMINATIONS)) self.amount = amount self.rest_client = rest_client if rest_client else create_default_rest_client( )
def create_systemd_file(dirname): """Create a systemd file that manages the starting/stopping of the gunicorn process. All processes are bound to a socket by default within the app directory. Args: dirname (string): directory the app is located in. Returns: bool: True if the process was successfully completed, False otherwise. """ rv = False plat = detect_os() if "darwin" in plat: raise exceptions.Two1Error(uxstring.UxString.unsupported_platform) appdir = dir_to_absolute(dirname) appname = absolute_path_to_foldername(appdir) with tempfile.NamedTemporaryFile() as tf: systemd_file = """[Unit] Description=gunicorn daemon for %s After=network.target [Service] WorkingDirectory=%s ExecStart=/usr/local/bin/gunicorn %s-server:app --workers 1 --bind unix:%s%s.sock --access-logfile %sgunicorn.access.log --error-logfile %sgunicorn.error.log [Install] WantedBy=default.target """ % ( # nopep8 appname, appdir, appname, appdir, appname, appdir, appdir) tf.write(systemd_file.encode()) tf.flush() try: subprocess.check_output([ "sudo", "cp", tf.name, "/etc/systemd/user/{}.service".format(appname) ]) subprocess.check_output([ "sudo", "chmod", "644", "/etc/systemd/user/{}.service".format(appname) ]) subprocess.check_output(['systemctl', '--user', 'enable', appname]) subprocess.check_output(['systemctl', '--user', 'start', appname]) rv = True except subprocess.CalledProcessError as e: raise e return rv
def buybitcoin(ctx, info, amount, denomination, history, price): """Buy bitcoin through Coinbase. \b To use this command, you need to connect your 21 account with your Coinbase account. You can find the instructions here: https://21.co/learn/21-buybitcoin/ \b Quote the price of 100000 satoshis. $ 21 buybitcoin 1000000 --price \b Buy 100000 satoshis from Coinbase. $ 21 buybitcoin 100000 satoshis You can use the following denominations: satoshis, bitcoins, and USD. \b Buy 5 dollars of bitcoin from Coinbase. $ 21 buybitcoin 5 usd \b See history of your purchases. $ 21 buybitcoin --history \b See the status of your 21 and Coinbase account integration. $ 21 buybitcoin --info The Bitcoins you purchase through this command will be deposited to your local wallet. If you have Instant Buy enabled on your Coinbase account, the purchase will be immediate. If you don't have Instant Buy, it may take up to 5 days for the purchase to be completed. """ exchange = "coinbase" if amount != 0.0: if denomination == '': confirmed = click.confirm( uxstring.UxString.default_price_denomination, default=True) if not confirmed: raise exceptions.Two1Error(uxstring.UxString.cancel_command) denomination = currency.Price.SAT amount = currency.Price(amount, denomination).satoshis return _buybitcoin(ctx, ctx.obj['config'], ctx.obj['client'], info, exchange, amount, history, price, denomination)
def detect_os(): """ Detect if the operating system that is running is either osx, debian-based or other. Returns: str: platform name Raises: OSError: if platform is not supported """ plat = platform.system().lower() if plat in ['debian', 'linux']: return 'debian' elif 'darwin' in plat: return 'darwin' else: raise exceptions.Two1Error(uxstring.UxString.unsupported_platform)
def buybitcoin_buy(config, client, exchange, amount): resp = client.get_coinbase_status() if not resp.ok: raise exceptions.Two1Error("Failed to get exchange status") coinbase = resp.json().get("coinbase") if not coinbase: return buybitcoin_config(config, client, exchange) try: get_price_quote(client, amount) except ValueError: return try: buy_bitcoin(client, amount) except click.exceptions.Abort: logger.info("\nPurchase canceled", fg="magenta")
def convert_amount_to_satoshis_with_prompt(amount, denomination): """ Converts and amount with denomination to satoshis. Prompts user if no denomination is specified. Args: amount (float): representing the amount to flush denomination (str): One of [satoshis, bitcoins, usd] Returns (int): converted amount to satoshis. """ if amount != 0.0: if denomination == '': confirmed = click.confirm( uxstring.UxString.default_price_denomination, default=True) if not confirmed: raise exceptions.Two1Error(uxstring.UxString.cancel_command) denomination = Price.SAT amount = Price(amount, denomination).satoshis else: amount = None return amount
def buybitcoin_history(config, client, exchange): resp = client.get_coinbase_status() if not resp.ok: raise exceptions.Two1Error("Failed to get exchange status") coinbase = resp.json()["coinbase"] if not coinbase: # Not linked, prompt user to info return buybitcoin_config(config, client, exchange) else: resp = client.get_coinbase_history() history = resp.json()["history"] lines = [uxstring.UxString.coinbase_history_title] for entry in history: amount = entry["amount"] deposit_status = entry["deposit_status"] payout_time = util.format_date(entry["payout_time"]) payout_address = entry["payout_address"] description = "N/A" if deposit_status == "COMPLETED": description = uxstring.UxString.coinbase_wallet_completed.format( payout_time) else: description = uxstring.UxString.coinbase_wallet_pending.format( payout_time) created = util.format_date(entry["created"]) lines.append( uxstring.UxString.coinbase_history.format( created, amount, payout_address, description)) if len(history) == 0: lines.append(uxstring.UxString.coinbase_no_bitcoins_purchased) prints = "\n\n".join(lines) logger.info(prints, pager=True)
def send(ctx, address, amount, denomination, use_unconfirmed, verbose): """Send a specified address some satoshis. \b Usage ----- Send 5000 satoshi from your on-chain balance to the Apache Foundation. $ 21 send 1BtjAzWGLyAavUkbw3QsyzzNDKdtPXk95D 5000 satoshis You can use the following denominations: satoshis, bitcoins, and USD. By default, this command uses only confirmed transactions and UTXOs to send coins. To use unconfirmed transactions, use the --use-unconfirmed flag. """ if denomination == '': confirmed = click.confirm(uxstring.UxString.default_price_denomination, default=True) if not confirmed: raise exceptions.Two1Error(uxstring.UxString.cancel_command) denomination = currency.Price.SAT price = currency.Price(amount, denomination) return _send(ctx.obj['wallet'], address, price.satoshis, verbose, use_unconfirmed)
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 create_account_on_bc(config, machine_auth): """ Creates an account for the current machine auth wallet Args: config (Config): config object used for getting .two1 information machine_auth (MachineAuthWallet): machine auth wallet used for authentication """ # 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 # Don't attempt to create an account if the user indicates they # already have an account (defaults to No) if click.confirm(uxstring.UxString.already_have_account): logger.info(uxstring.UxString.please_login) sys.exit() logger.info(uxstring.UxString.missing_account) email = None username = None fullname = None while True: if not fullname: fullname = click.prompt(uxstring.UxString.enter_name) if not email: email = click.prompt(uxstring.UxString.enter_email, type=EmailAddress()) # prompts for a username and password if not username: try: logger.info("") username = click.prompt(uxstring.UxString.enter_username, type=Username()) logger.info("") logger.info(uxstring.UxString.creating_account.format(username)) password = click.prompt(uxstring.UxString.set_new_password.format(username), hide_input=True, confirmation_prompt=True, type=Password()) except click.Abort: return try: # change the username of the given username rest_client = _rest_client.TwentyOneRestClient(two1.TWO1_HOST, machine_auth, username) rest_client.account_post(payout_address, email, password, fullname) # Do not continue creating an account because the UUID is invalid except exceptions.BitcoinComputerNeededError: raise except exceptions.ServerRequestError as ex: # handle various 400 errors from the server if ex.status_code == 400: if "error" in ex.data: error_code = ex.data["error"] # email exists if error_code == "TO401": logger.info(uxstring.UxString.email_exists.format(email)) email = None continue # username exists elif error_code == "TO402": logger.info(uxstring.UxString.username_exists.format(username)) username = None continue # unexpected 400 error else: raise exceptions.Two1Error( str(next(iter(ex.data.values()), "")) + "({})".format(ex.status_code)) # handle an invalid username format elif ex.status_code == 404: logger.info(uxstring.UxString.Error.invalid_username) # handle an error where a bitcoin computer is necessary elif ex.status_code == 403: r = ex.data if "detail" in r and "TO200" in r["detail"]: raise exceptions.UnloggedException(uxstring.UxString.max_accounts_reached) else: logger.info(uxstring.UxString.Error.account_failed) username = None # created account successfully else: logger.info(uxstring.UxString.payout_address.format(payout_address)) # save new username and password config.set("username", username) config.set("mining_auth_pubkey", machine_auth_pubkey_b64) config.save() break
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, mock_requests=False): """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. mock_requests (bool): skip 402 request and return an empty 200 response, intended for developer testing. 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, client) 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 if data: data, headers['Content-Type'] = _parse_post_data(data) # Make the paid request for the resource try: kwargs = {'max_price': maxprice, 'data': data, 'headers': headers} if data_file: kwargs['files'] = {'file': data_file} response = requests.request(method.lower(), resource, mock_requests=mock_requests, **kwargs) 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 bitrequests.InsufficientBalanceError as e: raise click.ClickException(e) except RequestException as e: raise exceptions.Two1Error('Requests exception: %s' % 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 # We will have paid iff response is a paid_response (regardless of # response.ok) if hasattr(response, 'amount_paid'): # 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) if not response.ok: sys.exit(1)
def create_config(dirname): """Create a nginx location file that redirects all requests with the prefix of the appname to the correct socket & process belonging to that app. i.e. curl 0.0.0.0/myapp1 should redirect requests to unix:/mysocketpath.sock @ the route / This allows for multiple apps to be namespaced and hosted on a single machine. Args: dirname (string): directory the app is located in. Returns: bool: True if the process was successfully completed, False otherwise. """ plat = detect_os() rv = False appdir = dir_to_absolute(dirname) appname = absolute_path_to_foldername(appdir) with tempfile.NamedTemporaryFile() as tf: nginx_site_includes_file = """location /%s { rewrite ^/%s(.*) /$1 break; proxy_pass http://unix:%s%s.sock; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }""" % (appname, appname, appdir, appname) tf.write(nginx_site_includes_file.encode()) tf.flush() try: subprocess.check_output([ "sudo", "cp", tf.name, "{}/etc/nginx/sites-available/{}".format( "/usr/local" if "darwin" in plat else "", appname) ]) subprocess.check_output([ "sudo", "chmod", "644", "{}/etc/nginx/sites-available/{}".format( "/usr/local" if "darwin" in plat else "", appname) ]) subprocess.check_output([ "sudo", "rm", "-f", "{}/etc/nginx/site-includes/{}".format( "/usr/local" if "darwin" in plat else "", appname) ]) subprocess.check_output([ "sudo", "ln", "-s", "{}/etc/nginx/sites-available/{}".format( "/usr/local" if "darwin" in plat else "", appname), "{}/etc/nginx/site-includes/{}".format( "/usr/local" if "darwin" in plat else "", appname) ]) if "darwin" in plat: if os.path.exists("/usr/local/var/run/nginx.pid"): subprocess.check_output("sudo nginx -s stop", shell=True) subprocess.check_output("sudo nginx", shell=True) else: subprocess.check_output( ["sudo", "service", "nginx", "restart"]) rv = True except subprocess.CalledProcessError as e: raise exceptions.Two1Error( uxstring.UxString.failed_configuring_nginx.format(e)) return rv