mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-06-01 19:43:03 +08:00
implement CLI commands for namespace_preorder, namespace_reveal, and namespace_ready. Include interactive wizards for configuring namespaces and acknowledging fees at each step.
This commit is contained in:
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user