diff --git a/blockstack_client/actions.py b/blockstack_client/actions.py index c384c57ff..ce57d7c6e 100644 --- a/blockstack_client/actions.py +++ b/blockstack_client/actions.py @@ -90,7 +90,7 @@ from rpc import local_api_connect, local_api_status, local_api_stop import rpc as local_rpc import config -from .config import configure_zonefile, set_advanced_mode, configure, get_utxo_provider_client, get_local_device_id, get_all_device_ids +from .config import configure_zonefile, set_advanced_mode, configure, get_utxo_provider_client, get_tx_broadcaster, get_local_device_id, get_all_device_ids from .constants import ( CONFIG_PATH, CONFIG_DIR, FIRST_BLOCK_TIME_UTC, APPROX_PREORDER_TX_LEN, APPROX_REGISTER_TX_LEN, @@ -108,7 +108,7 @@ from pybitcoin import serialize_transaction from .backend.blockchain import ( get_balance, is_address_usable, get_utxos, broadcast_tx, - can_receive_name, get_tx_confirmations, get_tx_fee + can_receive_name, get_tx_confirmations, get_tx_fee, get_block_height ) from .backend.registrar import get_wallet as registrar_get_wallet @@ -116,7 +116,8 @@ from .backend.registrar import get_wallet as registrar_get_wallet from .backend.nameops import ( estimate_preorder_tx_fee, estimate_register_tx_fee, estimate_update_tx_fee, estimate_transfer_tx_fee, - estimate_renewal_tx_fee, estimate_revoke_tx_fee + estimate_renewal_tx_fee, estimate_revoke_tx_fee, + do_namespace_preorder, do_namespace_reveal, do_namespace_ready ) from .backend.safety import * @@ -127,7 +128,7 @@ from .wallet import * from .keys import * from .proxy import * from .client import analytics_event -from .scripts import UTXOException, is_name_valid, is_valid_hash +from .scripts import UTXOException, is_name_valid, is_valid_hash, is_namespace_valid from .user import add_user_zonefile_url, remove_user_zonefile_url, user_zonefile_urls, \ make_empty_user_profile, user_zonefile_data_pubkey @@ -513,11 +514,59 @@ def cli_withdraw(args, password=None, interactive=True, wallet_keys=None, config return res +def get_price_and_fees( name_or_ns, operations, payment_privkey_info, owner_privkey_info, payment_address=None, owner_address=None, transfer_address=None, config_path=CONFIG_PATH, proxy=None ): + """ + Get the price and fees associated with a set of operations, using + a given owner and payment key. + Returns a dict with each operation as a key, and the prices in BTC and satoshis. + Returns {'error': ...} on failure + """ + + sg = ScatterGather() + res = get_operation_fees( name_or_ns, operations, sg, payment_privkey_info, owner_privkey_info, + proxy=proxy, config_path=config_path, payment_address=payment_address, + owner_address=owner_address, transfer_address=transfer_address ) + + if not res: + return {'error': 'Failed to get the requisite operation fees'} + + # do queries + sg.run_tasks() + + # get results + fees = interpret_operation_fees(operations, sg) + if 'error' in fees: + log.error("Failed to get all operation fees: {}".format(fees['error'])) + return {'error': 'Failed to get some operation fees: {}. Try again with `--debug` for details.'.format(fees['error'])} + + analytics_event('Name price', {}) + + # convert to BTC + btc_keys = [ + 'preorder_tx_fee', 'register_tx_fee', + 'update_tx_fee', 'total_estimated_cost', + 'name_price', 'transfer_tx_fee', 'renewal_tx_fee', + 'revoke_tx_fee', 'namespace_preorder_tx_fee', + 'namespace_reveal_tx_fee', 'namespace_ready_tx_fee', + 'name_import_tx_fee' + ] + + for k in btc_keys: + if k in fees.keys(): + v = { + 'satoshis': fees[k], + 'btc': satoshis_to_btc(fees[k]) + } + fees[k] = v + + return fees + + def cli_price(args, config_path=CONFIG_PATH, proxy=None, password=None): """ command: price help: Get the price to register a name - arg: name (str) 'Name to query' + arg: name_or_namespace (str) 'Name or namespace ID to query' opt: recipient (str) 'Address of the recipient, if not this wallet.' opt: operations (str) 'A CSV of operations to check.' """ @@ -525,7 +574,7 @@ def cli_price(args, config_path=CONFIG_PATH, proxy=None, password=None): proxy = get_default_proxy() if proxy is None else proxy password = get_default_password(password) - fqu = str(args.name) + name_or_ns = str(args.name_or_namespace) transfer_address = getattr(args, 'recipient', None) operations = getattr(args, 'operations', None) @@ -543,10 +592,13 @@ def cli_price(args, config_path=CONFIG_PATH, proxy=None, password=None): if 'error' in res: return res - error = check_valid_name(fqu) + error = check_valid_name(name_or_ns) if error: - return {'error': error} - + # must be valid namespace + ns_error = check_valid_namespace(name_or_ns) + if ns_error: + return {'error': 'Neither a valid name or namespace:\n * {}\n * {}'.format(error, ns_error)} + config_dir = os.path.dirname(config_path) wallet_path = os.path.join(config_dir, WALLET_FILENAME) @@ -573,40 +625,8 @@ def cli_price(args, config_path=CONFIG_PATH, proxy=None, password=None): log.debug("Could not get wallet keys from API server") - sg = ScatterGather() - res = get_operation_fees( fqu, operations, sg, payment_privkey_info, owner_privkey_info, - proxy=proxy, config_path=config_path, payment_address=payment_address, - owner_address=owner_address, transfer_address=transfer_address ) - - if not res: - return {'error': 'Failed to get the requisite operation fees'} - - # do queries - sg.run_tasks() - - # get results - fees = interpret_operation_fees(operations, sg) - if 'error' in fees: - log.error("Failed to get all operation fees: {}".format(fees['error'])) - return {'error': 'Failed to get some operation fees: {}. Try again with `--debug` for details.'.format(fees['error'])} - - analytics_event('Name price', {}) - - # convert to BTC - btc_keys = [ - 'preorder_tx_fee', 'register_tx_fee', - 'update_tx_fee', 'total_estimated_cost', - 'name_price', 'transfer_tx_fee', 'renewal_tx_fee', - 'revoke_tx_fee', - ] - - for k in btc_keys: - if k in fees.keys(): - v = { - 'satoshis': fees[k], - 'btc': satoshis_to_btc(fees[k]) - } - fees[k] = v + fees = get_price_and_fees( name_or_ns, operations, payment_privkey_info, owner_privkey_info, + payment_address=payment_address, owner_address=owner_address, transfer_address=transfer_address, config_path=config_path, proxy=proxy ) return fees @@ -661,7 +681,7 @@ def cli_import(args, config_path=CONFIG_PATH): def cli_names(args, config_path=CONFIG_DIR): """ command: names - help: Display the names owned by local addresses + help: Display the names owned by the wallet owner key """ result = {} @@ -2569,104 +2589,594 @@ def cli_name_import(args, config_path=CONFIG_PATH): return result -def cli_namespace_preorder(args, config_path=CONFIG_PATH): +def cli_namespace_preorder(args, config_path=CONFIG_PATH, interactive=True, proxy=None): """ command: namespace_preorder advanced help: Preorder a namespace arg: namespace_id (str) 'The namespace ID' - arg: privatekey (str) 'The private key to send and pay for the preorder' - opt: reveal_addr (str) 'The address of the keypair that will import names (automatically generated if not given)' + arg: payment_privkey (str) 'The private key to pay for the namespace' + arg: reveal_privkey (str) 'The private key that will import names' """ + import blockstack + + # NOTE: this does *not* go through the API. + # exposing this through the API is dangerous. + proxy = get_default_proxy() if proxy is None else proxy + config_dir = os.path.dirname(config_path) - if not local_api_status(config_dir=config_dir): - return {'error': 'API server not running. Please start it with `blockstack api start`.'} - - # BROKEN + nsid = str(args.namespace_id) + ns_privkey = str(args.payment_privkey) + ns_reveal_privkey = str(args.reveal_privkey) reveal_addr = None - if args.address is not None: - reveal_addr = str(args.address) + + try: + keylib.ECPrivateKey(ns_privkey) + reveal_addr = virtualchain.address_reencode( ecdsa_private_key(ns_reveal_privkey).public_key().address() ) + except: + return {'error': 'Invalid namespace private key'} + + print("Calculating fees...") + fees = get_price_and_fees(nsid, ['namespace_preorder', 'namespace_reveal', 'namespace_ready'], ns_privkey, ns_reveal_privkey, config_path=config_path, proxy=proxy ) + if 'error' in fees: + return fees - result = namespace_preorder( - str(args.namespace_id), - str(args.privatekey), - reveal_addr=reveal_addr - ) + msg = "".join([ + "\n", + "IMPORTANT: PLEASE READ THESE INSTRUCTIONS CAREFULLY\n", + "\n", + "You are about to preorder the namespace '{}'. It will cost about {} BTC ({} satoshi).\n".format(nsid, fees['total_estimated_cost']['btc'], fees['total_estimated_cost']['satoshis']), + "\n", + "Before you do, there are some things you should know.\n".format(nsid), + "\n", + "* Once you preorder the namespace, you will need to reveal it within 144 blocks (about 24 hours).\n", + " If you do not do so, then the namespace fee is LOST FOREVER and someone else can preorder it.\n", + " In addition, there is a very small (but non-zero) chance that someone else can preorder and reveal\n", + " the same namespace at the same time as you are. If this happens, your namespace fee is LOST FOREVER as well.\n", + "\n", + " The command to reveal the namespace is `blockstack namespace_reveal`. Please be prepared to run it\n", + " once your namespace-preorder transaction is confirmed.\n", + "\n", + "* You must launch the namespace within {} blocks (about 1 year) after it is revealed with the above command.\n".format(blockstack.BLOCKS_PER_YEAR), + " If you do not do so, then the namespace and all the names in it will DISAPPEAR, and you (or someone else)\n", + " will have to START THE NAMESPACE OVER FROM SCRATCH.\n", + "\n", + " The command to launch the namespace is `blockstack namespace_ready`. Please be prepared to run it\n", + " once your namespace-reveal transaction is confirmed, and once you have populated your namespace with\n", + " the initial names.\n", + "\n", + "* After you reveal the namespace but before you launch it, you can pre-populate it with names.\n", + " This is optional, but you have 1 year to import as many names as you want.\n", + " ONLY YOU WILL BE ABLE TO IMPORT NAMES until the namespace has been launched. Then, anyone can register names.\n", + "\n", + " The command to do so is `blockstack name_import`.\n", + "\n", + "If you want to test your namespace parameters before creating it, please consider trying it in our integration test\n", + "framework first. Instructions are at https://github.com/blockstack/blockstack-core/tree/master/integration_tests\n", + "\n", + "If you have any questions, please contact us at support@blockstack.com or via https://blockstack.slack.com\n", + "\n", + "Full cost breakdown:\n", + "{}".format(json.dumps(fees, indent=4, sort_keys=True)) + ]) + + print(msg) + + prompts = [ + "I acknowledge that I have read and understood the above instructions (yes/no) ", + "I acknowledge that this will cost {} BTC or more (yes/no) ".format(fees['total_estimated_cost']['btc']), + "I acknowledge that by not following these instructions, I may lose {} BTC (yes/no) ".format(fees['total_estimated_cost']['btc']), + "I acknowledge that I have tested my namespace in Blockstack's test mode (yes/no) ", + "I am ready to preorder this namespace (yes/no) " + ] - return result + for prompt in prompts: + while True: + if interactive: + proceed = raw_input("I acknowledge that I have read the above instructions. (yes/No) ") + else: + proceed = 'yes' + + if proceed.lower() == 'y': + print("Please type 'yes' or 'no'") + continue + + if proceed.lower() != 'yes': + return {'error': 'Aborting namespace preorder on user command'} + + break + + # proceed! + utxo_client = get_utxo_provider_client(config_path=config_path) + tx_broadcaster = get_tx_broadcaster(config_path=config_path) + res = do_namespace_preorder(nsid, int(fees['namespace_price']), ns_privkey, reveal_addr, utxo_client, tx_broadcaster, config_path=config_path, proxy=proxy, safety_checks=True) + return res -def cli_namespace_reveal(args, config_path=CONFIG_PATH): +def format_cost_table(namespace_id, base, coeff, bucket_exponents, nonalpha_discount, no_vowel_discount, block_height): + """ + Generate a string that contains a table of how much a name of a particular length will cost, + subject to particular discounts. + """ + import blockstack + + columns = [ + 'length', + 'price', + 'price, nonalpha', + 'price, no vowel', + 'price, both' + ] + + table = dict( [(c, [c]) for c in columns] ) + + namespace_params = { + 'base': base, + 'coeff': coeff, + 'buckets': bucket_exponents, + 'nonalpha_discount': nonalpha_discount, + 'no_vowel_discount': no_vowel_discount, + 'namespace_id': namespace_id + } + + for i in xrange(0, len(bucket_exponents)): + length = i+1 + price_normal = str(int(round( blockstack.price_name("a" * length, namespace_params, block_height) ))) + price_nonalpha = str(int(round( blockstack.price_name(("1a" * length)[:length], namespace_params, block_height) ))) + price_novowel = str(int(round( blockstack.price_name("b" * length, namespace_params, block_height) ))) + price_nonalpha_novowel = str(int(round( blockstack.price_name("1" * length, namespace_params, block_height) ))) + + len_str = str(length) + if i == len(bucket_exponents)-1: + len_str += '+' + + table['length'].append(len_str) + table['price'].append(price_normal) + table['price, nonalpha'].append(price_nonalpha) + table['price, no vowel'].append(price_novowel) + table['price, both'].append(price_nonalpha_novowel) + + col_widths = {} + for k in table.keys(): + max_width = reduce( lambda x, y: max(x,y), map( lambda p: len(p), table[k] ) ) + col_widths[k] = max_width + + row_template_parts = [' {{: >{}}} '.format(col_widths[c]) for c in columns] + header_template_parts = [' {{: <{}}} '.format(col_widths[c]) for c in columns] + + row_template = '|' + '|'.join(row_template_parts) + '|' + header_template = '|' + '|'.join(header_template_parts) + '|' + + table_str = header_template.format(*columns) + '\n' + table_str += '-' * (len(table_str) - 1) + '\n' + + for i in xrange(1, len(bucket_exponents)+1): + row_data = [table[c][i] for c in columns] + table_str += row_template.format(*row_data) + '\n' + + return table_str + + +def format_price_formula(namespace_id, block_height): + """ + Generate a string that encodes the generic price function + """ + import blockstack + + + exponent_str = " buckets[min(len(name)-1, 15)]\n" + numerator_str = " UNIT_COST * coeff * base \n" + divider_str = "cost(name) = -----------------------------------------------------\n" + denominator_str = " max(nonalpha_discount, no_vowel_discount) \n" + + formula_str = "(UNIT_COST = 100):\n" + \ + exponent_str + \ + numerator_str + \ + divider_str + \ + denominator_str + + return formula_str + + +def format_price_formula_worksheet(name, namespace_id, base, coeff, bucket_exponents, nonalpha_discount, no_vowel_discount, block_height): + """ + Generate a string that encodes the namespace price function for a particular name + """ + import blockstack + + if '.' in name: + if name.count('.') != 1: + return '\nThe specified name is invalid. Names may not have periods (.) in them\n' + + name_parts = name.split('.') + name = name_parts[0] + if name_parts[1] != namespace_id: + return '\nThe name specified does not belong to this namespace\n' + + full_name = '{}.{}'.format(name, namespace_id) + err = check_valid_name(full_name) + if err: + return "\nFully-qualified name: {}\n{}\n".format(full_name, err) + + namespace_params = { + 'base': base, + 'coeff': coeff, + 'buckets': bucket_exponents, + 'nonalpha_discount': nonalpha_discount, + 'no_vowel_discount': no_vowel_discount, + 'namespace_id': namespace_id + } + + def has_no_vowel_discount(n): + return sum( [n.lower().count(v) for v in ["a", "e", "i", "o", "u", "y"]] ) == 0 + + def has_nonalpha_discount(n): + return sum( [n.lower().count(v) for v in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-", "_"]] ) > 0 + + discount = 1 + + # no vowel discount? + if has_no_vowel_discount(name): + # no vowels! + discount = max( discount, no_vowel_discount ) + + # non-alpha discount? + if has_nonalpha_discount(name): + # non-alpha! + discount = max( discount, nonalpha_discount ) + + eval_numerator_str = "{} * {} * {}".format(blockstack.NAME_COST_UNIT * blockstack.get_epoch_price_multiplier(block_height, namespace_id), coeff, base) + eval_exponent_str = "{}".format(bucket_exponents[min(len(name)-1, 15)]) + eval_denominator_str = "{}".format(discount) + + eval_divider_str = "cost({}) = ".format(name) + left_pad = len(eval_divider_str) + + numerator_len = len(eval_numerator_str) + len(eval_exponent_str) + denominator_len = len(eval_denominator_str) + + padded_exponent_str = (" " * (left_pad + len(eval_numerator_str))) + eval_exponent_str + '\n' + padded_numerator_str = (" " * left_pad) + eval_numerator_str + '\n' + padded_divider_str = eval_divider_str + ("-" * numerator_len) + (" = {}".format(int(blockstack.price_name(name, namespace_params, block_height)))) + '\n' + padded_denominator_str = (" " * (left_pad + (numerator_len / 2) - (denominator_len / 2))) + eval_denominator_str + '\n' + + formula_str = "Worksheet:\n" + \ + "\n" + \ + "len({}) = {}\n".format(name, len(name)) + \ + "buckets[min(len(name)-1, 15)] = buckets[min({}, 15)] = buckets[{}] = {}\n".format(len(name)-1, min(len(name)-1, 15), bucket_exponents[min(len(name)-1, 15)]) + \ + "nonalpha_discount = {}\n".format( nonalpha_discount if has_nonalpha_discount(name) else 1 ) + \ + "no_vowel_discount = {}\n".format( no_vowel_discount if has_no_vowel_discount(name) else 1 ) + \ + "max(nonalpha_discount, no_vowel_discount) = max({}, {}) = {}\n".format(nonalpha_discount if has_nonalpha_discount(name) else 1, no_vowel_discount if has_no_vowel_discount(name) else 1, discount) + \ + "\n" + \ + padded_exponent_str + \ + padded_numerator_str + \ + padded_divider_str + \ + padded_denominator_str + \ + "\n" + + return formula_str + + +def cli_namespace_reveal(args, interactive=True, config_path=CONFIG_PATH, proxy=None): """ command: namespace_reveal advanced - help: Reveal a namespace and set its pricing parameters + help: Reveal a namespace and interactively set its pricing parameters arg: namespace_id (str) 'The namespace ID' - arg: addr (str) 'The address of the keypair that will import names (given in the namespace preorder)' - arg: lifetime (int) 'The lifetime (in blocks) for each name. Negative means "never expires".' - arg: coeff (int) 'The multiplicative coefficient in the price function.' - arg: base (int) 'The exponential base in the price function.' - arg: bucket_exponents (str) 'A 16-field CSV of name-length exponents in the price function.' - arg: nonalpha_discount (int) 'The denominator that defines the discount for names with non-alpha characters.' - arg: no_vowel_discount (int) 'The denominator that defines the discount for names without vowels.' - arg: privatekey (str) 'The private key of the import keypair (whose address is `addr` above).' + arg: payment_privkey (str) 'The private key that paid for the namespace' + arg: reveal_privkey (str) 'The private key that will import names' """ + # NOTE: this will not use the RESTful API. + # it is too dangerous to expose this to web browsers. + import blockstack + + namespace_id = str(args.namespace_id) + privkey = str(args.payment_privkey) + reveal_privkey = str(args.reveal_privkey) + reveal_addr = None - config_dir = os.path.dirname(config_path) - if not local_api_status(config_dir=config_dir): - return {'error': 'API server not running. Please start it with `blockstack api start`.'} + res = is_namespace_valid(namespace_id) + if not res: + return {'error': 'Invalid namespace ID'} - # BROKEN - bucket_exponents = args.bucket_exponents.split(',') - if len(bucket_exponents) != 16: - msg = '`bucket_exponents` must be a 16-value CSV of integers' - return {'error': msg} + try: + ecdsa_private_key(privkey) + reveal_addr = virtualchain.address_reencode( ecdsa_private_key(reveal_privkey).public_key().address() ) + except: + return {'error': 'Invalid private keys'} - for i in range(len(bucket_exponents)): + infinite_lifetime = 0xffffffff # means "infinite" to blockstack-core + + # sane defaults + lifetime = int(getattr(args, 'lifetime', infinite_lifetime)) + coeff = int(getattr(args, 'coeff', 4)) + base = int(getattr(args, 'base', 4)) + bucket_exponents_str = getattr(args, 'buckets', "15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0") + nonalpha_discount = int(getattr(args, 'nonalpha_discount', 2)) + no_vowel_discount = int(getattr(args, 'no_vowel_discount', 5)) + + def parse_bucket_exponents(exp_str): + + bucket_exponents_strs = exp_str.split(',') + if len(bucket_exponents_strs) != 16: + raise Exception('bucket_exponents must be a 16-value comma-separated list of integers') + + bucket_exponents = [] + for i in xrange(0, len(bucket_exponents_strs)): + try: + bucket_exponents.append( int(bucket_exponents_strs[i]) ) + assert 0 <= bucket_exponents[i] + assert bucket_exponents[i] < 16 + except (ValueError, AssertionError) as e: + raise Exception('bucket_exponents must contain integers between 0 and 15, inclusively') + + return bucket_exponents + + def print_namespace_configuration(params): + print("Namespace ID: {}".format(namespace_id)) + print("Name lifetimes: {}".format(params['lifetime'] if params['lifetime'] != infinite_lifetime else "infinite")) + print("Price coefficient: {}".format(params['coeff'])) + print("Price base: {}".format(params['base'])) + print("Price bucket exponents: {}".format(params['buckets'])) + print("Non-alpha discount: {}".format(params['nonalpha_discount'])) + print("No-vowel discount: {}".format(params['no_vowel_discount'])) + print("") + + formula_str = format_price_formula(namespace_id, block_height) + price_table_str = format_cost_table(namespace_id, params['base'], params['coeff'], params['buckets'], + params['nonalpha_discount'], params['no_vowel_discount'], block_height) + + print("Name price formula:") + print(formula_str) + print("") + print("Name price table:") + print(price_table_str) + + print("") + + + bbs = None + try: + bbs = parse_bucket_exponents(bucket_exponents_str) + except Exception as e: + if BLOCKSTACK_DEBUG: + log.exception(e) + + return {'error': 'Invalid bucket exponents. Must be a list of 16 integers between 0 and 15.'} + + namespace_params = { + 'lifetime': lifetime, + 'coeff': coeff, + 'base': base, + 'buckets': bbs, + 'nonalpha_discount': nonalpha_discount, + 'no_vowel_discount': no_vowel_discount + } + + block_height = get_block_height(config_path=config_path) + + options = { + '1': { + 'msg': 'Set name lifetime in blocks (positive integer between 1 and {}, or "infinite")'.format(2**32 - 1), + 'var': 'lifetime', + 'parse': lambda x: infinite_lifetime if x == "infinite" else int(x) + }, + '2': { + 'msg': 'Set price coefficient (positive integer between 1 and 255)', + 'var': 'coeff', + 'parse': lambda x: int(x) + }, + '3': { + 'msg': 'Set base price (positive integer between 1 and 255)', + 'var': 'base', + 'parse': lambda x: int(x) + }, + '4': { + 'msg': 'Set price bucket exponents (16 comma-separated integers, each between 1 and 15)', + 'var': 'buckets', + 'parse': lambda x: parse_bucket_exponents(x) + }, + '5': { + 'msg': 'Set non-alphanumeric character discount (positive integer between 1 and 15)', + 'var': 'nonalpha_discount', + 'parse': lambda x: int(x) + }, + '6': { + 'msg': 'Set no-vowel discount (positive integer between 1 and 15)', + 'var': 'no_vowel_discount', + 'parse': lambda x: int(x) + }, + '7': { + 'msg': 'Show name price formula', + 'input': 'Enter name: ', + 'callback': lambda inp: print(format_price_formula_worksheet(inp, namespace_id, namespace_params['base'], namespace_params['coeff'], + namespace_params['buckets'], namespace_params['nonalpha_discount'], + namespace_params['no_vowel_discount'], block_height)) + }, + '8': { + 'msg': 'Show price table', + 'callback': lambda: print(print_namespace_configuration(namespace_params)), + }, + '9': { + 'msg': 'Done', + 'break': True, + }, + } + option_order = options.keys() + option_order.sort() + + print_namespace_configuration(namespace_params) + + while interactive: + + print("What would you like to do?") + + for opt in option_order: + print("({}) {}".format(opt, options[opt]['msg'])) + + print("") + + selection = None + value = None + while True: + selection = raw_input("({}-{}) ".format(option_order[0], option_order[-1])) + if selection not in options: + print("Please select between {} and {}".format(option_order[0], option_order[-1])) + continue + else: + break + + if options[selection].has_key('var'): + value_str = raw_input("New value for '{}': ".format(options[selection]['var'])) + old_value = namespace_params[ options[selection]['var'] ] + try: + value = options[selection]['parse'](value_str) + namespace_params[ options[selection]['var'] ] = value + assert blockstack.namespacereveal.namespacereveal_sanity_check( namespace_id, blockstack.BLOCKSTACK_VERSION, namespace_params['lifetime'], + namespace_params['coeff'], namespace_params['base'], namespace_params['buckets'], + namespace_params['nonalpha_discount'], namespace_params['no_vowel_discount'] ) + + except Exception as e: + if BLOCKSTACK_DEBUG: + log.exception(e) + + print("Invalid value for '{}'".format(options[selection]['var'])) + namespace_params[ options[selection]['var'] ] = old_value + + print_namespace_configuration(namespace_params) + continue + + elif options[selection].has_key('input'): + inpstr = options[selection]['input'] + inp = raw_input(inpstr) + options[selection]['callback'](inp) + continue + + elif options[selection].has_key('callback'): + options[selection]['callback']() + continue + + elif options[selection].has_key('break'): + break + + else: + raise Exception("BUG: selection {}".format(selection)) + + if not interactive: + # still check this try: - bucket_exponents[i] = int(bucket_exponents[i]) - assert 0 <= bucket_exponents[i] < 16 - except (ValueError, AssertionError) as e: - msg = '`bucket_exponents` must contain integers between 0 and 15, inclusively.' - return {'error': msg} + assert blockstack.namespacereveal.namespacereveal_sanity_check( namespace_id, blockstack.BLOCKSTACK_VERSION, namespace_params['lifetime'], + namespace_params['coeff'], namespace_params['base'], namespace_params['buckets'], + namespace_params['nonalpha_discount'], namespace_params['no_vowel_discount'] ) + except Exception as e: + if BLOCKSTACK_DEBUG: + log.exception(e) - lifetime = int(args.lifetime) - if lifetime < 0: - lifetime = 0xffffffff # means "infinite" to blockstack-server + return {'error': 'Invalid namespace parameters'} - # BUG: undefined function - result = namespace_reveal( - str(args.namespace_id), - str(args.addr), - lifetime, - int(args.coeff), - int(args.base), - bucket_exponents, - int(args.nonalpha_discount), - int(args.no_vowel_discount), - str(args.privatekey) - ) + # do we have enough btc? + print("Calculating fees...") + fees = get_price_and_fees(namespace_id, ['namespace_reveal'], privkey, reveal_privkey, config_path=config_path, proxy=proxy ) + if 'error' in fees: + return fees - return result + if 'warnings' in fees: + return {'error': 'Not enough information for fee calculation: {}'.format( ", ".join(["'{}'".format(w) for w in fees['warnings']]) )} + + # got the params we wanted + print("This is the final configuration for your namespace:") + print_namespace_configuration(namespace_params) + print("You will NOT be able to change this once it is set.") + print("Reveal address: {}".format(reveal_addr)) + print("Payment address: {}".format(virtualchain.address_reencode(ecdsa_private_key(privkey).public_key().address()))) + print("Transaction cost breakdown:\n{}".format(json.dumps(fees, indent=4, sort_keys=True))) + print("") + + while interactive: + proceed = raw_input("Proceed? (yes/no) ") + if proceed.lower() == 'y': + print("Please type 'yes' or 'no'") + continue + + if proceed.lower() != 'yes': + return {'error': 'Aborted namespace reveal'} + + break + + # proceed! + utxo_client = get_utxo_provider_client(config_path=config_path) + tx_broadcaster = get_tx_broadcaster(config_path=config_path) + res = do_namespace_reveal(namespace_id, reveal_addr, namespace_params['lifetime'], namespace_params['coeff'], namespace_params['base'], namespace_params['buckets'], + namespace_params['nonalpha_discount'], namespace_params['no_vowel_discount'], privkey, utxo_client, tx_broadcaster, config_path=config_path, proxy=proxy, safety_checks=True) + + return res -def cli_namespace_ready(args, config_path=CONFIG_PATH): +def cli_namespace_ready(args, interactive=True, config_path=CONFIG_PATH, proxy=None): """ command: namespace_ready advanced help: Mark a namespace as ready arg: namespace_id (str) 'The namespace ID' - arg: privatekey (str) 'The private key of the keypair that imports names' + arg: reveal_privkey (str) 'The private key used to import names' """ - config_dir = os.path.dirname(config_path) - if not local_api_status(config_dir=config_dir): - return {'error': 'API server not running. Please start it with `blockstack api start`.'} - # BROKEN - result = namespace_ready( - str(args.namespace_id), - str(args.privatekey) - ) + import blockstack + namespace_id = str(args.namespace_id) + reveal_privkey = str(args.reveal_privkey) - return result + res = is_namespace_valid(namespace_id) + if not res: + return {'error': 'Invalid namespace ID'} + + try: + reveal_addr = virtualchain.address_reencode( ecdsa_private_key(reveal_privkey).public_key().address() ) + except: + return {'error': 'Invalid private keys'} + + # do we have enough btc? + print("Calculating fees...") + fees = get_price_and_fees(namespace_id, ['namespace_ready'], reveal_privkey, reveal_privkey, config_path=config_path, proxy=proxy ) + if 'error' in fees: + return fees + + if 'warnings' in fees: + return {'error': 'Not enough information for fee calculation: {}'.format( ", ".join(["'{}'".format(w) for w in fees['warnings']]) )} + + namespace_rec = get_namespace_blockchain_record(namespace_id, proxy=proxy) + if 'error' in namespace_rec: + return namespace_rec + + if namespace_rec['ready']: + return {'error': 'Namespace is already launched'} + + launch_deadline = namespace_rec['reveal_block'] + blockstack.NAMESPACE_REVEAL_EXPIRE + + print("Cost breakdown:\n{}".format(json.dumps(fees, indent=4, sort_keys=True))) + print("This command will launch the namespace '{}'".format(namespace_id)) + print("Once launched, *anyone* can register a name in it.") + print("This namespace must be launched by block {}, so please run this command before block {}.".format(launch_deadline, launch_deadline - 144)) + print("THIS CANNOT BE UNDONE.") + print("") + while True: + proceed = None + if interactive: + proceed = raw_input("Proceed? (yes/no) ") + else: + proceed = 'yes' + + if proceed.lower() == 'y': + print("Please type 'yes' or 'no'") + continue + + if proceed.lower() != 'yes': + return {'error': 'Namespace launch aborted'} + + break + + # proceed! + utxo_client = get_utxo_provider_client(config_path=config_path) + tx_broadcaster = get_tx_broadcaster(config_path=config_path) + res = do_namespace_ready(namespace_id, reveal_privkey, utxo_client, tx_broadcaster, config_path=config_path, proxy=proxy, safety_checks=True) + return res def cli_put_mutable(args, config_path=CONFIG_PATH, password=None, proxy=None): @@ -3095,9 +3605,18 @@ def cli_get_all_names(args, config_path=CONFIG_PATH): return result +def cli_get_all_namespaces(args, config_path=CONFIG_PATH): + """ + command: get_all_namespaces + help: Get the list of namespaces + """ + result = get_all_namespaces() + return result + + def cli_get_names_in_namespace(args, config_path=CONFIG_PATH): """ - command: get_names_in_namespace + command: get_names_in_namespace advanced help: Get the names in a given namespace, optionally paginating through them arg: namespace_id (str) 'The ID of the namespace to query' opt: offset (int) 'The offset into the sorted list of names'