Files
stacks-puppet-node/blockstack_client/actions.py
Aaron Blankstein 93d07d68b3 This reverts us *all* the way back to 523cf405d7 -- this removes all changes to support the token file from this branch.
Revert "added example request and response for PUT /v1/wallet/keys/owner to api docs"

This reverts commit d52ee4b31e.

Revert "cutting down on the verbosity of logging outputs -- registrar now only prints 1 line on wakeups. storage drivers are concatenated into 1 line"

This reverts commit 87e3e7ab0d.

Revert "adding dropbox as a default storage driver to load, and switched default 'required' drivers to 'disk,dropbox'"

This reverts commit 9471b0a20a.

Revert "adding test case for issue 483, which *also* required fixing the app session schema to handle empty string methods a little bit more gracefully"

This reverts commit 32efc99d62.

Revert "bugfix for the address reencoding in get_zonefile -- checks to see if the address is an address before trying to reencode"

This reverts commit 1488013b93.

Revert "Merge branch 'rc-0.14.3' of github.com:blockstack/blockstack-core into rc-0.14.3"

This reverts commit f75ab67960, reversing
changes made to fe863bcd3c.

Revert "don't create the metadata dir"

This reverts commit fe863bcd3c.

Revert "make all metadata directories inside the critical section"

This reverts commit e66236abd2.

Revert "don't cast 'None' to string by accident"

This reverts commit c6250d5349.

Revert "force string"

This reverts commit e72d43d0be.

Revert "add unbound proxy variable"

This reverts commit 7f1f7e9731.

Revert "return raw zonefile"

This reverts commit 51e858428d.

Revert "force string"

This reverts commit 1ce371644f.

Revert "force string"

This reverts commit 5353cb1015.

Revert "require virtualchain rc-0.14.3 and jsontokens-py 0.0.4"

This reverts commit 346f042db7.

Revert "Merge branch 'rc-0.14.3' of https://github.com/blockstack/blockstack-core into rc-0.14.3"

This reverts commit 1fa1de3e54, reversing
changes made to 523cf405d7.
2017-07-10 14:59:23 -04:00

5661 lines
195 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function
"""
Blockstack-client
~~~~~
copyright: (c) 2014-2015 by Halfmoon Labs, Inc.
copyright: (c) 2016 by Blockstack.org
This file is part of Blockstack-client.
Blockstack-client is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Blockstack-client is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Blockstack-client. If not, see <http://www.gnu.org/licenses/>.
"""
"""
Every method that begins with `cli_` in this module
is matched to an action to be taken, based on the
CLI input.
CLI-accessible begin with `cli_`. For exmample, "blockstack transfer ..."
will cause `cli_transfer(...)` to be called.
The following conventions apply to `cli_` methods here:
* Each will always take a Namespace (from ArgumentParser.parse_known_args())
as its first argument.
* Each will return a dict with the requested information. The key 'error'
will be set to indicate an error condition.
If you want to add a new command-line action, implement it here. This
will make it available not only via the command-line, but also via the
local RPC interface and the test suite.
Use the _cli_skel method below a template to create new functions.
"""
import sys
import json
import traceback
import os
import re
import errno
import virtualchain
from socket import error as socket_error
import time
import blockstack_zones
import blockstack_profiles
import requests
import binascii
from decimal import Decimal
import string
import jsontokens
requests.packages.urllib3.disable_warnings()
import logging
logging.disable(logging.CRITICAL)
# Hack around absolute paths
current_dir = os.path.abspath(os.path.dirname(__file__))
parent_dir = os.path.abspath(current_dir + '/../')
sys.path.insert(0, parent_dir)
from blockstack_client import (
delete_immutable, delete_mutable, get_all_names, get_consensus_at,
get_immutable, get_immutable_by_name, get_mutable, get_name_blockchain_record,
get_name_zonefile, get_nameops_at, get_names_in_namespace, get_names_owned_by_address,
get_namespace_blockchain_record, get_namespace_cost,
is_user_zonefile, list_immutable_data_history, list_update_history,
list_zonefile_history, lookup_snv, put_immutable, put_mutable, zonefile_data_replicate
)
from blockstack_client.profile import put_profile, delete_profile, get_profile, \
profile_add_device_id, profile_remove_device_id, profile_list_accounts, profile_get_account, \
profile_put_account, profile_delete_account
from rpc import local_api_connect, local_api_status
import rpc as local_rpc
import config
from .config import configure_zonefile, configure, get_utxo_provider_client, get_tx_broadcaster
from .constants import (
CONFIG_PATH, CONFIG_DIR,
FIRST_BLOCK_MAINNET, NAME_UPDATE,
BLOCKSTACK_DEBUG, TX_MIN_CONFIRMATIONS, DEFAULT_SESSION_LIFETIME
)
from .storage import get_driver_urls, get_storage_handlers, sign_data_payload, \
get_zonefile_data_hash
from .backend.blockchain import (
get_balance, get_utxos, broadcast_tx, select_utxos,
get_tx_confirmations, get_tx_fee, get_tx_fee_per_byte, get_block_height
)
from .backend.registrar import get_wallet as registrar_get_wallet
from .backend.nameops import (
do_namespace_preorder, do_namespace_reveal, do_namespace_ready,
do_name_import
)
from .backend.safety import *
from .backend.queue import queuedb_remove, queuedb_find, queue_append
from .backend.queue import extract_entry as queue_extract_entry
from .wallet import *
from .keys import *
from .proxy import *
from .client import analytics_event
from .scripts import UTXOException, is_name_valid, is_valid_hash, is_namespace_valid
from .user import make_empty_user_profile, user_zonefile_data_pubkey
from .tx import serialize_tx, sign_tx
from .zonefile import make_empty_zonefile, url_to_uri_record
from .utils import exit_with_error, satoshis_to_btc, ScatterGather
from .app import app_publish, app_get_config, app_get_resource, \
app_put_resource, app_delete_resource
from .data import datastore_mkdir, datastore_rmdir, make_datastore_info, put_datastore, delete_datastore, \
datastore_getfile, datastore_putfile, datastore_deletefile, datastore_listdir, datastore_stat, \
datastore_rmtree, datastore_get_id, datastore_get_privkey, \
datastore_getinode, datastore_get_privkey, \
make_mutable_data_info, data_blob_parse, data_blob_serialize, make_mutable_data_tombstones, sign_mutable_data_tombstones
from .schemas import OP_URLENCODED_PATTERN, OP_NAME_PATTERN, OP_USER_ID_PATTERN, OP_BASE58CHECK_PATTERN
import virtualchain
from virtualchain.lib.ecdsalib import *
log = config.get_logger()
"""
The _cli_skel method is provided as a template for developers of
cli_ methods.
NOTE: extra cli arguments may be included in function params
NOTE: $NAME_OF_COMMAND must not have embedded whitespaces.
NOTE: As a security precaution, a cli_ function is not accessible
NOTE: via RPC by default. It has to be enabled explicitly. See below.
NOTE: If the "rpc" pragma is present, then the method will be
NOTE: accessible via the RPC interface of the background process
NOTE: Help string in arg and opt must be enclosed in single quotes.
The entire docstr must strictly adhere to this convention:
command: $NAME_OF_COMMAND [rpc]
help: $HELP_STRING
arg: $ARG_NAME ($ARG_TYPE) '$ARG_HELP'
arg: $ARG_NAME ($ARG_TYPE) '$ARG_HELP'
opt: $OPT_ARG_NAME ($OPT_ARG_TYPE) '$OPT_ARG_HELP'
opt: $OPT_ARG_NAME ($OPT_ARG_TYPE) '$OPT_ARG_HELP'
"""
def _cli_skel(args, config_path=CONFIG_PATH):
"""
command: skel
help: Skeleton cli function - developer template
arg: foo (str) 'A required argument - foo'
opt: bar (int) 'An optional argument - bar'
"""
result = {}
# update result as needed
if 'error' in result:
# ensure meaningful error message
result['error'] = 'Error generating skel'
return result
# continue processing the result
return result
def wallet_ensure_exists(config_path=CONFIG_PATH):
"""
Check that the wallet exists
Return {'status': True} on success
Return {'error': ...} on error
"""
if not wallet_exists(config_path=config_path):
return {'error': 'No wallet exists for {}. Please create one with `blockstack setup`'.format(config_path)}
return {'status': True}
def load_zonefile_from_string(fqu, zonefile_data, check_current=True):
"""
Load a zonefile from a string, which can be
either JSON or text. Verify that it is
well-formed and current.
Return {'status': True, 'zonefile': the serialized zonefile data (as a string), 'parsed_zonefile': ...} on success.
Return {'error': ..., 'nonstandard': True/False, 'identical': True/False} if the zonefile is nonstandard and/or identical
"""
# is this a new, standard zonefile?
nonstandard = False
identical = False
user_data = None
user_zonefile = None
try:
user_data = json.loads(zonefile_data)
except:
log.debug('Zonefile is not a serialized JSON string; try parsing as text')
try:
user_data = blockstack_zones.parse_zone_file(zonefile_data)
user_data = dict(user_data) # force dict. e.g if not defaultdict
except Exception as e:
if BLOCKSTACK_DEBUG is not None:
log.exception(e)
nonstandard = True
if user_data is not None:
try:
user_zonefile = blockstack_zones.make_zone_file(user_data)
except Exception as e:
if BLOCKSTACK_DEBUG:
log.exception(e)
log.error('Nonstandard zonefile')
nonstandard = True
# sanity checks on the standard-ness
if not nonstandard:
if fqu != user_data.get('$origin', ''):
log.error('Zonefile is missing or has invalid $origin')
nonstandard = True
if '$ttl' not in user_data:
log.error('Zonefile is missing a TTL')
nonstandard = True
if not is_user_zonefile(user_data):
log.error("Zonefile does not match standard schema")
nonstandard = True
try:
ttl = int(user_data['$ttl'])
assert ttl >= 0
except Exception as e:
log.error("Zonefile has an invalid $ttl; must be a positive integer")
nonstandard = True
if check_current:
current = False
if not nonstandard and user_data is not None:
current = is_zonefile_current(fqu, user_data)
else:
current = is_zonefile_data_current(fqu, zonefile_data)
if current:
log.debug('Zonefile data is same as current zonefile; update not needed.')
identical = True
if user_zonefile is not None and not identical and not nonstandard:
return {'status': True, 'zonefile': user_zonefile, 'parsed_zonefile': user_data, 'identical': identical, 'nonstandard': nonstandard}
elif nonstandard:
return {'error': 'nonstandard zonefile', 'identical': identical, 'nonstandard': nonstandard}
else:
return {'error': 'identical zonefile', 'zonefile': user_zonefile, 'parsed_zonefile': user_data, 'identical': identical, 'nonstandard': nonstandard}
def get_default_password(password):
"""
Get the default password
"""
return password if password is not None else get_secret("BLOCKSTACK_CLIENT_WALLET_PASSWORD")
def get_default_interactive(interactive):
"""
Get default interactive setting
"""
if os.environ.get("BLOCKSTACK_CLIENT_INTERACTIVE_YES", None) == "1":
return False
else:
return interactive
def cli_setup(args, config_path=CONFIG_PATH, password=None):
"""
command: setup
help: Set up your Blockstack installation
"""
password = get_default_password(password)
interactive = get_default_interactive(True)
ret = {}
log.debug("Set up config file")
# are we configured?
opts = config.setup_config(config_path=config_path, interactive=interactive)
if 'error' in opts:
return opts
class WalletSetupArgs(object):
pass
wallet_args = WalletSetupArgs()
# is our wallet ready?
res = cli_setup_wallet(wallet_args, interactive=interactive, config_path=config_path, password=password)
if 'error' in res:
return res
if 'backup_wallet' in res:
ret['backup_wallet'] = res['backup_wallet']
ret['status'] = True
return ret
def cli_configure(args, config_path=CONFIG_PATH):
"""
command: configure
help: Interactively configure the client
"""
interactive = True
force = True
if os.environ.get("BLOCKSTACK_CLIENT_INTERACTIVE_YES", None) == "1":
interactive = False
force = False
opts = configure(interactive=interactive, force=force, config_file=config_path)
result = {}
result['path'] = opts['blockstack-client']['path']
return result
def cli_balance(args, config_path=CONFIG_PATH):
"""
command: balance
help: Get the account balance
opt: min_confs (int) 'The minimum confirmations of transactions to include in balance'
"""
config_dir = os.path.dirname(config_path)
wallet_path = os.path.join(config_dir, WALLET_FILENAME)
res = wallet_ensure_exists(config_path=config_path)
if 'error' in res:
return res
min_confs = getattr(args, 'min_confs', None)
result = {}
addresses = []
satoshis = 0
satoshis, addresses = get_total_balance(
wallet_path=wallet_path, config_path=config_path, min_confs = min_confs)
if satoshis is None:
log.error('Failed to get balance')
# contains error
return addresses
# convert to BTC
btc = float(Decimal(satoshis / 1e8))
for address_info in addresses:
address_info['bitcoin'] = float(Decimal(address_info['balance'] / 1e8))
address_info['satoshis'] = address_info['balance']
del address_info['balance']
result = {
'total_balance': {
'satoshis': int(satoshis),
'bitcoin': btc
},
'addresses': addresses
}
return result
def cli_withdraw(args, password=None, interactive=True, wallet_keys=None, config_path=CONFIG_PATH):
"""
command: withdraw
help: Transfer funds out of the Blockstack wallet to a new address
arg: address (str) 'The recipient address'
opt: amount (int) 'The amount to withdraw (defaults to all)'
opt: message (str) 'A message to include with the payment (up to 40 bytes)'
opt: min_confs (int) 'The minimum confirmations for oustanding transactions'
opt: tx_only (str) 'If "True", only return the transaction'
"""
config_dir = os.path.dirname(config_path)
wallet_path = os.path.join(config_dir, WALLET_FILENAME)
password = get_default_password(password)
recipient_addr = str(args.address)
amount = getattr(args, 'amount', None)
message = getattr(args, 'message', None)
min_confs = getattr(args, 'min_confs', TX_MIN_CONFIRMATIONS)
tx_only = getattr(args, 'tx_only', False)
if min_confs is None:
min_confs = TX_MIN_CONFIRMATIONS
if tx_only:
if tx_only.lower() in ['1', 'yes', 'true']:
tx_only = True
else:
tx_only = False
else:
tx_only = False
if not re.match(OP_BASE58CHECK_PATTERN, recipient_addr):
log.debug("recipient = {}".format(recipient_addr))
return {'error': 'Invalid address'}
if amount is not None and not isinstance(amount, int):
log.debug("amount = {}".format(amount))
return {'error': 'Invalid amount'}
if not isinstance(min_confs, int):
log.debug("min_confs = {}".format(min_confs))
return {'error': 'Invalid min confs'}
if not isinstance(tx_only, bool):
log.debug("tx_only = {}".format(tx_only))
return {'error': 'Invalid tx_only'}
if message:
message = str(message)
if len(message) > virtualchain.bitcoin_blockchain.MAX_DATA_LEN:
return {'error': 'Message must be {} bytes or less (got {})'.format(virtualchain.bitcoin_blockchain.MAX_DATA_LEN, len(message))}
res = wallet_ensure_exists(config_path=config_path)
if 'error' in res:
return res
if wallet_keys is None:
res = load_wallet(password=password, wallet_path=wallet_path, interactive=interactive, include_private=True)
if 'error' in res:
return res
wallet_keys = res['wallet']
send_addr, _, _ = get_addresses_from_file(config_dir=config_dir, wallet_path=wallet_path)
inputs = get_utxos(str(send_addr), min_confirmations=min_confs, config_path=config_path)
if len(inputs) == 0:
log.error("No UTXOs for {}".format(send_addr))
return {'error': 'Failed to find UTXOs for wallet payment address'}
total_value = sum(inp['value'] for inp in inputs)
def mktx( amt, tx_fee ):
"""
Make the transaction with the given fee
"""
change = 0
selected_inputs = []
if amt is None:
# total transfer, minus tx fee
log.debug("No amount given; assuming all ({})".format(total_value - tx_fee))
amt = total_value - tx_fee
if amt < 0:
log.error("Dust: total value = {}, tx fee = {}".format(total_value, tx_fee))
return {'error': 'Cannot withdraw dust'}
selected_inputs = select_utxos(inputs, amt)
else:
selected_inputs = select_utxos(inputs, amt)
change = virtualchain.calculate_change_amount(selected_inputs, amt, tx_fee)
log.debug("Withdraw {}, tx fee {}".format(amt, tx_fee))
outputs = [
{'script': virtualchain.make_payment_script(recipient_addr),
'value': amt},
]
if amt < total_value and change > 0:
# need change and tx fee
outputs.append(
{'script': virtualchain.make_payment_script(send_addr),
"value": change}
)
if message:
outputs = [
{"script": virtualchain.make_data_script(binascii.hexlify(message)),
"value": 0} ] + outputs
serialized_tx = serialize_tx(selected_inputs, outputs)
signed_tx = sign_tx(serialized_tx, wallet_keys['payment_privkey'])
return signed_tx
tx = mktx(amount, 0)
tx_fee = get_tx_fee(tx, config_path=config_path)
tx = mktx(amount, tx_fee)
if tx_only:
return {'status': True, 'tx': tx}
log.debug("Withdraw {} from {} to {}".format(amount, send_addr, recipient_addr))
log.debug("Withdraw {} from {} to {}".format(amount, send_addr, recipient_addr))
res = broadcast_tx( tx, config_path=config_path )
if 'error' in res:
res['errno'] = errno.EIO
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
"""
# first things first: get fee per byte
tx_fee_per_byte = get_tx_fee_per_byte(config_path=config_path)
if tx_fee_per_byte is None:
log.error("Unable to calculate fee per byte")
return {'error': 'Unable to get fee estimate'}
log.debug("Get operation fees for {}".format(", ".join(operations)))
sg = ScatterGather()
res = get_operation_fees( name_or_ns, operations, sg, payment_privkey_info, owner_privkey_info, tx_fee_per_byte,
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'}
if 'error' in res:
return res
# 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, interactive=True):
"""
command: price
help: Get the price to register a name
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.'
"""
proxy = get_default_proxy() if proxy is None else proxy
password = get_default_password(password)
name_or_ns = str(args.name_or_namespace)
transfer_address = getattr(args, 'recipient', None)
operations = getattr(args, 'operations', None)
if transfer_address is not None:
transfer_address = str(transfer_address)
if operations is not None:
operations = operations.split(',')
else:
operations = ['preorder', 'register', 'update']
if transfer_address:
operations.append('transfer')
res = wallet_ensure_exists(config_path=config_path)
if 'error' in res:
return res
error = check_valid_name(name_or_ns)
if error:
if 'preorder' in operations:
# this means this is a pricecheck on a NAME, not a namespace
return {'error': 'Not a valid name: \n * {}'.format(error)}
else:
# 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)
payment_privkey_info, owner_privkey_info = None, None
payment_address, owner_address, data_pubkey = (
get_addresses_from_file(config_dir=config_dir, wallet_path=wallet_path)
)
if local_api_status(config_dir=config_dir):
# API server is running. Use actual wallet keys.
log.debug("Try to get wallet keys from API server")
try:
wallet_keys = get_wallet_keys(config_path, password)
if 'error' in wallet_keys:
return wallet_keys
payment_privkey_info = wallet_keys['payment_privkey']
owner_privkey_info = wallet_keys['owner_privkey']
except (OSError, IOError) as e:
# backend is not running; estimate with addresses
if BLOCKSTACK_DEBUG is not None:
log.exception(e)
log.error("Could not get wallet keys from API server")
return {'error': 'Could not load wallet keys from API server. Try `blockstack api stop` and `blockstack api start`'}
else:
# unlock
log.warning("API server is not running; unlocking wallet directly")
res = load_wallet(password=password, wallet_path=wallet_path, interactive=interactive, include_private=True)
if 'error' in res:
return res
if res['migrated']:
return {'error': 'Wallet is in legacy format. Please migrate it with `setup_wallet`'}
wallet_keys = res['wallet']
payment_privkey_info = wallet_keys['payment_privkey']
owner_privkey_info = wallet_keys['owner_privkey']
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
def cli_deposit(args, config_path=CONFIG_PATH):
"""
command: deposit
help: Display the address with which to receive bitcoins
"""
config_dir = os.path.dirname(config_path)
wallet_path = os.path.join(config_dir, WALLET_FILENAME)
res = wallet_ensure_exists(config_path=config_path)
if 'error' in res:
return res
result = {}
result['message'] = 'Send bitcoins to the address specified.'
result['address'], owner_address, data_address = (
get_addresses_from_file(wallet_path=wallet_path)
)
return result
def cli_import(args, config_path=CONFIG_PATH):
"""
command: import
help: Display the address with which to receive names
"""
config_dir = os.path.dirname(config_path)
wallet_path = os.path.join(config_dir, WALLET_FILENAME)
res = wallet_ensure_exists(config_path=config_path)
if 'error' in res:
return res
result = {}
result['message'] = (
'Send the name you want to receive to the address specified.'
)
payment_address, result['address'], data_address = (
get_addresses_from_file(wallet_path=wallet_path)
)
return result
def cli_names(args, config_path=CONFIG_DIR):
"""
command: names
help: Display the names owned by the wallet owner key
"""
result = {}
config_dir = os.path.dirname(config_path)
wallet_path = os.path.join(config_dir, WALLET_FILENAME)
res = wallet_ensure_exists(config_path=config_path)
if 'error' in res:
return res
result['names_owned'] = get_all_names_owned(wallet_path)
result['addresses'] = get_owner_addresses_and_names(wallet_path)
return result
def cli_get_registrar_info(args, config_path=CONFIG_PATH, queues=None):
"""
command: get_registrar_info advanced
help: Get information about the backend registrar queues
"""
queues = ['preorder', 'register', 'update', 'transfer', 'renew', 'revoke', 'name_import'] if queues is None else queues
config_dir = os.path.dirname(config_path)
conf = config.get_config(config_path)
rpc = local_api_connect(config_path=config_path)
if rpc is None:
return {'error': 'API endpoint not running. Please start it with `api start`'}
try:
current_state = rpc.backend_state()
except Exception, e:
if BLOCKSTACK_DEBUG:
log.exception(e)
log.error("Failed to contact Blockstack daemon")
return {'error': 'Failed to contact blockstack daemon. Please ensure that it is running with the `api` command.'}
if 'error' in current_state:
return current_state
queue_types = dict( [(queue_name, []) for queue_name in queues] )
def format_queue_entry(entry):
"""
Determine data to display for a queue entry.
Return {'name': ..., 'tx_hash': ..., 'confirmations': ...}
"""
new_entry = {}
new_entry['name'] = entry['fqu']
confirmations = get_tx_confirmations(
entry['tx_hash'], config_path=config_path
)
confirmations = 0 if confirmations is None else confirmations
new_entry['confirmations'] = confirmations
new_entry['tx_hash'] = entry['tx_hash']
if 'errors' in entry:
new_entry['errors'] = entry['errors']
return new_entry
def remove_dups(preorder_queue, register_queue):
"""
Omit duplicates between preorder and register queue
"""
for entry in register_queue:
name = entry['name']
for check_entry in preorder_queue:
if check_entry['name'] == name:
preorder_queue.remove(check_entry)
# extract entries
for entry in current_state:
entry_type = entry['type']
if entry_type not in queue_types:
log.error('Unknown entry type "{}"'.format(entry_type))
continue
queue_types[entry['type']].append(format_queue_entry(entry))
# clean up duplicates
remove_dups(queue_types['preorder'], queue_types['register'])
# remove empty entries
ret = {}
for queue_type in queue_types:
if queue_types[queue_type]:
ret[queue_type] = queue_types[queue_type]
return ret
def get_server_info(config_path=CONFIG_PATH, get_local_info=False):
"""
Get information about the running server,
and any pending operations.
"""
config_dir = os.path.dirname(config_path)
conf = config.get_config(config_path)
resp = getinfo()
result = {}
result['cli_version'] = VERSION
if 'error' in resp:
result['server_alive'] = False
result['server_error'] = resp['error']
return result
result['server_alive'] = True
result['server_host'] = (
resp.get('server_host') or
conf.get('server')
)
result['server_port'] = (
resp.get('server_port') or
int(conf.get('port'))
)
result['server_version'] = (
resp.get('server_version') or
resp.get('blockstack_version') or
resp.get('blockstore_version')
)
if result['server_version'] is None:
raise Exception('Missing server version')
result['last_block_processed'] = (
resp.get('last_block_processed') or
resp.get('last_block') or
resp.get('blocks')
)
if result['last_block_processed'] is None:
raise Exception('Missing height of block last processed')
result['last_block_seen'] = (
resp.get('last_block_seen') or
resp.get('blockchain_blocks') or
resp.get('bitcoind_blocks')
)
if result['last_block_seen'] is None:
raise Exception('Missing height of last block seen')
try:
result['consensus_hash'] = resp['consensus']
except KeyError:
raise Exception('Missing consensus hash')
if not get_local_info:
return result
queue_info = cli_get_registrar_info(None, config_path=config_path)
if 'error' not in queue_info:
result['queues'] = queue_info
else:
return queue_info
return result
def cli_info(args, config_path=CONFIG_PATH):
"""
command: info
help: Get details about pending name commands
"""
return get_server_info(config_path=config_path, get_local_info=True)
def cli_ping(args, config_path=CONFIG_PATH):
"""
command: ping
help: Check server status and get server details
"""
return get_server_info(config_path=config_path)
def cli_lookup(args, config_path=CONFIG_PATH):
"""
command: lookup
help: Get the zone file and profile for a particular name
arg: name (str) 'The name to look up'
"""
data = {}
blockchain_record = None
fqu = str(args.name)
error = check_valid_name(fqu)
if error:
return {'error': error}
try:
blockchain_record = get_name_blockchain_record(fqu)
except socket_error:
return {'error': 'Error connecting to server.'}
if 'error' in blockchain_record:
return blockchain_record
if 'value_hash' not in blockchain_record:
return {'error': '{} has no profile'.format(fqu)}
if blockchain_record.get('revoked', False):
msg = 'Name is revoked. Use get_name_blockchain_record for details.'
return {'error': msg}
try:
res = get_profile(
str(args.name), name_record=blockchain_record, include_raw_zonefile=True, use_legacy=True, use_legacy_zonefile=True
)
if 'error' in res:
return res
data['profile'] = res['profile']
data['zonefile'] = res['raw_zonefile']
except Exception as e:
log.exception(e)
msg = 'Failed to look up name\n{}'
return {'error': msg.format(traceback.format_exc())}
result = data
analytics_event('Name lookup', {})
return result
def cli_whois(args, config_path=CONFIG_PATH):
"""
command: whois
help: Look up the blockchain info for a name
arg: name (str) 'The name to look up'
"""
result = {}
record, fqu = None, str(args.name)
error = check_valid_name(fqu)
if error:
return {'error': error}
try:
record = get_name_blockchain_record(fqu)
except socket_error:
exit_with_error('Error connecting to server.')
if 'error' in record:
return record
if record.get('revoked', False):
msg = 'Name is revoked. Use get_name_blockchain_record for details.'
return {'error': msg}
history = record.get('history', {})
update_heights = []
try:
assert isinstance(history, dict)
# all items must be ints
update_heights = sorted(int(_) for _ in history)
except (AssertionError, ValueError):
return {'error': 'Invalid record data returned'}
result['block_preordered_at'] = record['preorder_block_number']
result['block_renewed_at'] = record['last_renewed']
result['last_transaction_id'] = record['txid']
result['owner_address'] = record['address']
result['owner_script'] = record['sender']
value_hash = record.get('value_hash', None)
if value_hash in [None, 'null', '']:
result['has_zonefile'] = False
else:
result['has_zonefile'] = True
result['zonefile_hash'] = value_hash
if update_heights:
result['last_transaction_height'] = update_heights[-1]
expire_block = record.get('expire_block', None)
if expire_block is not None:
result['expire_block'] = expire_block
analytics_event('Whois', {})
return result
def get_wallet_with_backoff(config_path):
"""
Get the wallet, but keep trying
in the case of a ECONNREFUSED
(i.e. the API daemon could still be initializing)
Return the wallet keys on success (as a dict)
return {'error': ...} on error
"""
wallet_keys = None
i = 0
for i in range(3):
try:
wallet_keys = get_wallet(config_path=config_path)
return wallet_keys
except (IOError, OSError) as se:
if se.errno == errno.ECONNREFUSED:
# still spinning up
log.debug("Still spinning up")
time.sleep(i + 1)
continue
raise
if i == 3:
log.error('Failed to get_wallet')
wallet_keys = {'error': 'Failed to connect to API daemon'}
return wallet_keys
def get_wallet_keys(config_path, password):
"""
Load up the wallet keys
Return the dict with the keys on success
Return {'error': ...} on failure
"""
config_dir = os.path.dirname(config_path)
if local_rpc.is_api_server(config_dir):
# can return directly
return registrar_get_wallet(config_path=config_path)
wallet_path = os.path.join(config_dir, WALLET_FILENAME)
res = wallet_ensure_exists(config_path=config_path)
if 'error' in res:
return res
status = local_api_status(config_dir=os.path.dirname(config_path))
if not status:
return {'error': 'API endpoint not running. Please start it with `blockstack api start`'}
if not is_wallet_unlocked(config_dir=config_dir):
log.debug('unlocking wallet ({})'.format(config_dir))
res = unlock_wallet(config_dir=config_dir, password=password)
if 'error' in res:
log.error('unlock_wallet: {}'.format(res['error']))
return res
if res.has_key('legacy') and res['legacy']:
log.error("Wallet is in legacy format. Please migrate it to the latest version with `setup_wallet`.")
return {'error': 'Wallet is in legacy format. Please migrate it to the latest version with `setup_wallet.`'}
return get_wallet_with_backoff(config_path)
def prompt_invalid_zonefile():
"""
Prompt the user whether or not to replicate
an invalid zonefile
"""
if os.environ.get("BLOCKSTACK_CLIENT_INTERACTIVE_YES", None) == "1":
return True
warning_str = (
'\nWARNING! This zone file data does not look like a zone file.\n'
'If you proceed to use this data, no one will be able to look\n'
'up your profile or any data you replicate with Blockstack.\n\n'
'Proceed? (y/N): '
)
proceed = raw_input(warning_str)
return proceed.lower() in ['y']
def prompt_transfer( new_owner_address ):
"""
Prompt whether or not to proceed with a transfer
"""
if os.environ.get("BLOCKSTACK_CLIENT_INTERACTIVE_YES", None) == "1":
return True
warning_str = (
'\nWARNING! This will transfer your name to a different owner.\n'
'The recipient\'s address will be: {}\n.'
'THIS CANNOT BE UNDONE OR CANCELED.\n'
'\n'
'Proceed? (y/N): '
)
proceed = raw_input(warning_str.format(new_owner_address))
return proceed.lower() in ['y']
def is_valid_path(path):
"""
Is the given string a valid path?
"""
if not isinstance(path, str):
return False
# while not technically denied by POSIX, paths usually
# have only printable characters and without the "weird"
# whitespace characters
valid_chars = set(string.printable) - set("\t\n\r\x0b\x0c")
filtered_string = filter(lambda x: x in valid_chars, path)
return filtered_string == path
def analyze_zonefile_string(fqu, zonefile_data, force_data=False, check_current=True, proxy=None):
"""
Figure out what to do with a zone file data string, based on whether or not
we can prompt the user and whether or not we expect a standard zonefile.
if @force_data is True, then the zonefile_data will be treated as raw data.
Otherwise, it will be considered to be a path
Returns: {
'is_string': True/False # whether or not the zone file string is a raw zone file
'is_path': True/False # whether or not the zone file string is a path to a file on disk
'downloaded': True/False # whether or not the zone file was fetched remotely
'identical': True/False # whether or not the zone file is identical to the name's current zone file
'nonstandard': True/False # whether or not the zone file follows the standard format
'raw_zonefile': str # the raw zone file data. will be equal to zonefile_data if it is not None
'zonefile': dict # the parsed standard zone file (or None if nonstandard)
'zonefile_str': str # the serialized zone file data. Will be equal to 'raw_zonefile' if nonstandard; otherwise is equal to serialized zonefile if standard
}
Return {'error': ...} on error
"""
ret = {}
zonefile_data_exists_on_disk = zonefile_data is not None and is_valid_path(zonefile_data) and os.path.exists(zonefile_data)
if zonefile_data is None:
# fetch remotely
zonefile_data_res = get_name_zonefile(
fqu, proxy=proxy, raw_zonefile=True
)
if 'error' not in zonefile_data_res:
zonefile_data = zonefile_data_res['zonefile']
else:
log.warning('Failed to fetch zonefile: {}'.format(zonefile_data_res['error']))
# zone file is not given; we had to fetch it
ret['downloaded'] = True
ret['raw_zonefile'] = zonefile_data
ret['is_path'] = False
ret['is_string'] = False
elif zonefile_data_exists_on_disk and not force_data:
# this sure looks like a path
try:
with open(zonefile_data) as f:
zonefile_data = f.read()
except:
raise Exception("Invalid arguments: failed to read file")
# loaded from path
ret['downloaded'] = False
ret['raw_zonefile'] = zonefile_data
ret['is_path'] = True
ret['is_string'] = False
elif force_data:
# string given
ret['downloaded'] = False
ret['raw_zonefile'] = zonefile_data
ret['is_path'] = False
ret['is_string'] = True
else:
if force_data:
return {'error': 'Invalid argument: no data given'}
else:
return {'error': 'Invalid argument: no such file or directory: {}'.format(zonefile_data)}
# load zonefile, if given
user_data_res = load_zonefile_from_string(fqu, zonefile_data, check_current=check_current)
# propagate identical and nonstandard...
ret['identical'] = user_data_res['identical']
ret['nonstandard'] = user_data_res['nonstandard']
if user_data_res.has_key('zonefile'):
ret['zonefile'] = user_data_res['zonefile']
if user_data_res.has_key('parsed_zonefile'):
ret['zonefile_str'] = blockstack_zones.make_zone_file(user_data_res['parsed_zonefile'])
else:
ret['zonefile_str'] = ret['raw_zonefile']
return ret
def cli_register(args, config_path=CONFIG_PATH, force_data=False,
cost_satoshis=None, interactive=True, password=None, proxy=None):
"""
command: register
help: Register a blockchain ID
arg: name (str) 'The blockchain ID to register'
opt: zonefile (str) 'The path to the zone file for this name'
opt: recipient (str) 'The recipient address, if not this wallet'
opt: min_confs (int) 'The minimum number of confirmations on the initial preorder'
opt: unsafe_reg (str) 'Should we aggressively register the name (ie, use low min confs)'
"""
# NOTE: if force_data == True, then the zonefile will be the zonefile text itself, not a path.
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`.'}
proxy = get_default_proxy(config_path) if proxy is None else proxy
password = get_default_password(password)
conf = config.get_config(config_path)
assert conf
res = wallet_ensure_exists(config_path=config_path)
if 'error' in res:
return res
result = {}
fqu = str(args.name)
user_zonefile = getattr(args, 'zonefile', None)
transfer_address = getattr(args, 'recipient', None)
min_payment_confs = getattr(args, 'min_confs', TX_MIN_CONFIRMATIONS)
unsafe_reg = getattr(args, 'unsafe_reg', 'False')
if unsafe_reg.lower() in ('true', 't', 'yes', '1'):
unsafe_reg = True
else:
unsafe_reg = False
# name must be well-formed
error = check_valid_name(fqu)
if error:
return {'error': error}
if min_payment_confs is None:
min_payment_confs = TX_MIN_CONFIRMATIONS
else:
log.debug("Use UTXOs with a minimum of {} confirmations".format(min_payment_confs))
if transfer_address:
if not re.match(OP_BASE58CHECK_PATTERN, transfer_address):
return {'error': 'Not a valid address'}
user_profile = None
if user_zonefile:
zonefile_info = analyze_zonefile_string(fqu, user_zonefile, force_data=force_data, proxy=proxy)
if 'error' in zonefile_info:
log.error("Failed to analyze user zonefile: {}".format(zonefile_info['error']))
return {'error': zonefile_info['error']}
if zonefile_info.get('nonstandard'):
log.warning("Non-standard zone file")
if interactive:
proceed = prompt_invalid_zonefile()
if not proceed:
return {'error': 'Non-standard zone file'}
user_zonefile = zonefile_info['zonefile_str']
else:
# make a default zonefile
_, _, data_pubkey = get_addresses_from_file(config_dir=config_dir)
if not data_pubkey:
return {'error': 'No data key in wallet. Please add one with `setup_wallet`'}
user_zonefile_dict = make_empty_zonefile(fqu, data_pubkey)
user_zonefile = blockstack_zones.make_zone_file(user_zonefile_dict)
# only make an empty profile if user didn't give a zonefile.
# if we have a data key, then make the empty profile
if not transfer_address:
# registering for this wallet. Put an empty profile
_, _, data_pubkey = get_addresses_from_file(config_dir=config_dir)
if not data_pubkey:
return {'error': 'No data key in wallet. Please add one with `setup_wallet`'}
user_profile = make_empty_user_profile()
# operation checks (API server only)
if local_rpc.is_api_server(config_dir=config_dir):
# find tx fee, and do sanity checks
wallet_keys = get_wallet_keys(config_path, password)
if 'error' in wallet_keys:
return wallet_keys
owner_privkey_info = wallet_keys['owner_privkey']
payment_privkey_info = wallet_keys['payment_privkey']
operations = ['preorder', 'register', 'update']
required_checks = ['is_name_available', 'is_payment_address_usable', 'owner_can_receive']
if transfer_address:
operations.append('transfer')
required_checks.append('recipient_can_receive')
res = check_operations( fqu, operations, owner_privkey_info, payment_privkey_info, min_payment_confs=min_payment_confs,
transfer_address=transfer_address, required_checks=required_checks, config_path=config_path, proxy=proxy )
if 'error' in res:
return res
opchecks = res['opchecks']
if cost_satoshis is not None:
if opchecks['name_price'] > cost_satoshis:
return {'error': 'Invalid cost: expected {}, got {}'.format(opchecks['name_price'], cost_satoshis)}
else:
cost_satoshis = opchecks['name_price']
if transfer_address == '':
transfer_address = None
if interactive and os.environ.get("BLOCKSTACK_CLIENT_INTERACTIVE_YES", None) != "1":
try:
print("Calculating total registration costs for {}...".format(fqu))
class PriceArgs(object):
pass
price_args = PriceArgs()
price_args.name_or_namespace = fqu
price_args.recipient = transfer_address
costs = cli_price( price_args, config_path=config_path, password=password, proxy=proxy )
if 'error' in costs:
return {'error': 'Failed to get name costs. Please try again with `--debug` to see error messages.'}
cost = costs['total_estimated_cost']
input_prompt = (
'Registering {} will cost about {} BTC.\n'
'Use `blockstack price {}` for a cost breakdown\n'
'\n'
'The entire process takes 48 confirmations, or about 5 hours.\n'
'You need to have Internet access during this time period, so\n'
'this program can send the right transactions at the right\n'
'times.\n\n'
'Continue? (y/N): '
)
input_prompt = input_prompt.format(fqu, cost['btc'], fqu)
user_input = raw_input(input_prompt)
user_input = user_input.lower()
if user_input.lower() != 'y':
print('Not registering.')
exit(0)
except KeyboardInterrupt:
print('\nExiting.')
exit(0)
# forward along to RESTful server (or if we're the RESTful server, call the registrar method)
log.debug("Preorder {}, zonefile={}, profile={}, recipient={} min_confs={}".format(fqu, user_zonefile, user_profile, transfer_address, min_payment_confs))
rpc = local_api_connect(config_path=config_path)
assert rpc
try:
resp = rpc.backend_preorder(fqu, cost_satoshis, user_zonefile, user_profile,
transfer_address, min_payment_confs,
unsafe_reg = unsafe_reg)
except Exception as e:
log.exception(e)
return {'error': 'Error talking to server, try again.'}
if 'error' in resp:
log.debug('RPC error: {}'.format(resp['error']))
return resp
if (not 'success' in resp or not resp['success']) and 'message' in resp:
return {'error': resp['message']}
result = resp
if local_rpc.is_api_server(config_dir):
# log this
total_estimated_cost = {'total_estimated_cost': opchecks['total_estimated_cost']}
analytics_event('Register name', total_estimated_cost)
return result
def cli_update(args, config_path=CONFIG_PATH, password=None,
interactive=True, proxy=None, nonstandard=False,
force_data=False):
"""
command: update
help: Set the zone file for a blockchain ID
arg: name (str) 'The name to update.'
opt: data (str) 'A path to a file with the zone file data.'
opt: nonstandard (str) 'If true, then do not validate or parse the zone file.'
"""
# NOTE: if force_data == True, then the zonefile will be the zonefile text itself, not a path.
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`.'}
if not interactive and getattr(args, 'data', None) is None:
return {'error': 'Zone file data required in non-interactive mode'}
proxy = get_default_proxy() if proxy is None else proxy
password = get_default_password(password)
if hasattr(args, 'nonstandard') and not nonstandard:
if args.nonstandard is not None and args.nonstandard.lower() in ['yes', '1', 'true']:
nonstandard = True
conf = config.get_config(config_path)
assert conf
res = wallet_ensure_exists(config_path=config_path)
if 'error' in res:
return res
fqu = str(args.name)
error = check_valid_name(fqu)
if error:
return {'error': error}
zonefile_data_path_or_string = None
downloaded = False
if getattr(args, 'data', None) is not None:
zonefile_data_path_or_string = str(args.data)
if not local_rpc.is_api_server(config_dir=config_dir):
# verify that we own the name before trying to edit its zonefile
_, owner_address, _ = get_addresses_from_file(config_dir=config_dir)
assert owner_address
res = get_names_owned_by_address( owner_address, proxy=proxy )
if 'error' in res:
return res
if fqu not in res:
return {'error': 'This wallet does not own this name'}
zonefile_info = analyze_zonefile_string(fqu, zonefile_data_path_or_string, force_data=force_data, proxy=proxy)
if 'error' in zonefile_info:
log.error("Failed to analyze zone file: {}".format(zonefile_info['error']))
return {'error': zonefile_info['error']}
if zonefile_info['identical'] and not zonefile_info['downloaded']:
log.error("Zone file has not changed")
return {'error': 'Zone file matches the current name hash; not updating'}
# load zonefile, if given
user_data_txt, user_data_hash, user_zonefile_dict = None, None, {}
zonefile_data = zonefile_info['zonefile_str']
if not zonefile_info['nonstandard'] and (not zonefile_info['identical'] or zonefile_info['downloaded']):
# standard zone file that is not identital to what we have now, or standard zonefile that we downloaded and wish to edit
user_data_txt = zonefile_data
user_data_hash = get_zonefile_data_hash(zonefile_data)
user_zonefile_dict = blockstack_zones.parse_zone_file(zonefile_data)
else:
if not interactive:
if zonefile_data is None or nonstandard:
log.warning('Using non-zonefile data')
else:
return {'error': 'Zone file not updated (invalid)'}
# not a standard zonefile (but maybe that's okay! ask the user)
if zonefile_data is not None and interactive:
# something invalid here. prompt overwrite
proceed = prompt_invalid_zonefile()
if not proceed:
return {'error': 'Zone file not updated'}
else:
nonstandard = True
user_data_txt = zonefile_data
if zonefile_data is not None:
user_data_hash = get_zonefile_data_hash(zonefile_data)
# open the zonefile editor
_, _, data_pubkey = get_addresses_from_file(config_dir=config_dir)
if data_pubkey is None:
return {'error': 'No data public key set in the wallet. Please use `blockstack setup_wallet` to fix this.'}
if interactive and not nonstandard:
# configuration wizard!
if user_zonefile_dict is None:
user_zonefile_dict = make_empty_zonefile(fqu, data_pubkey)
new_zonefile = configure_zonefile(
fqu, user_zonefile_dict, data_pubkey
)
if new_zonefile is None:
# zonefile did not change; nothing to do
return {'error': 'Zonefile did not change. No update sent.'}
user_zonefile_dict = new_zonefile
user_data_txt = blockstack_zones.make_zone_file(user_zonefile_dict)
user_data_hash = get_zonefile_data_hash(user_data_txt)
# forward along to RESTful server (or registrar)
log.debug("Update {}, zonefile={}, zonefile_hash={}".format(fqu, user_data_txt, user_data_hash))
rpc = local_api_connect(config_path=config_path)
assert rpc
try:
# NOTE: already did safety checks
resp = rpc.backend_update(fqu, user_data_txt, None, None )
except Exception as e:
log.exception(e)
return {'error': 'Error talking to server, try again.'}
if 'error' in resp:
log.debug('RPC error: {}'.format(resp['error']))
return resp
if (not 'success' in resp or not resp['success']) and 'message' in resp:
return {'error': resp['message']}
analytics_event('Update name', {})
resp['zonefile_hash'] = user_data_hash
return resp
def cli_transfer(args, config_path=CONFIG_PATH, password=None, interactive=False, proxy=None):
"""
command: transfer
help: Transfer a blockchain ID to a new owner
arg: name (str) 'The name to transfer'
arg: address (str) 'The address (base58check-encoded pubkey hash) to receive the name'
"""
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`.'}
proxy = get_default_proxy() if proxy is None else proxy
password = get_default_password(password)
conf = config.get_config(config_path)
assert conf
res = wallet_ensure_exists(config_path=config_path)
if 'error' in res:
return res
fqu = str(args.name)
transfer_address = str(args.address)
error = check_valid_name(fqu)
if error:
return {'error': error}
if interactive:
res = prompt_transfer(transfer_address)
if not res:
return {'error': 'Transfer cancelled.'}
# do the name transfer via the RESTful server (or registrar)
rpc = local_api_connect(config_path=config_path)
assert rpc
try:
resp = rpc.backend_transfer(fqu, transfer_address)
except Exception as e:
log.exception(e)
return {'error': 'Error talking to server, try again.'}
if 'error' in resp:
log.debug('RPC error: {}'.format(resp['error']))
return resp
if (not 'success' in resp or not resp['success']) and 'message' in resp:
return {'error': resp['message']}
analytics_event('Transfer name', {})
return resp
def cli_renew(args, config_path=CONFIG_PATH, interactive=True, password=None, proxy=None, cost_satoshis=None):
"""
command: renew
help: Renew a blockchain ID
arg: name (str) 'The blockchain ID to renew'
"""
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`.'}
if proxy is None:
proxy = get_default_proxy(config_path)
password = get_default_password(password)
conf = config.get_config(config_path)
assert conf
res = wallet_ensure_exists(config_path=config_path)
if 'error' in res:
return res
fqu = str(args.name)
error = check_valid_name(fqu)
if error:
return {'error': error}
if interactive:
print("Calculating total renewal costs for {}...".format(fqu))
# get the costs...
class PriceArgs(object):
pass
price_args = PriceArgs()
price_args.name_or_namespace = fqu
price_args.operations = 'renewal'
costs = cli_price( price_args, config_path=config_path, password=password, proxy=proxy )
if 'error' in costs:
return {'error': 'Failed to get renewal costs. Please try again with `--debug` to see error messages.'}
cost = costs['total_estimated_cost']
if cost_satoshis is None:
cost_satoshis = costs['name_price']['satoshis']
if not local_rpc.is_api_server(config_dir=config_dir):
# also verify that we own the name
_, owner_address, _ = get_addresses_from_file(config_dir=config_dir)
assert owner_address
res = get_names_owned_by_address( owner_address, proxy=proxy )
if 'error' in res:
return res
if fqu not in res:
return {'error': 'This wallet does not own this name'}
if interactive and os.environ.get("BLOCKSTACK_CLIENT_INTERACTIVE_YES", None) != "1":
try:
input_prompt = (
'Renewing {} will cost about {} BTC.\n'
'Use `blockstack price {} "" renewal` for a cost breakdown\n'
'\n'
'The entire process takes 12 confirmations, or about 2 hours.\n'
'You need to have Internet access during this time period, so\n'
'this program can send the right transactions at the right\n'
'times.\n\n'
'Continue? (y/N): '
)
input_prompt = input_prompt.format(fqu, cost['btc'], fqu)
user_input = raw_input(input_prompt)
user_input = user_input.lower()
if user_input.lower() != 'y':
print('Not renewing.')
exit(0)
except KeyboardInterrupt:
print('\nExiting.')
exit(0)
rpc = local_api_connect(config_path=config_path)
assert rpc
log.debug("Renew {} for {} BTC".format(fqu, cost_satoshis))
try:
resp = rpc.backend_renew(fqu, cost_satoshis)
except Exception as e:
log.exception(e)
return {'error': 'Error talking to server, try again.'}
total_estimated_cost = {'total_estimated_cost': cost}
if 'error' in resp:
log.debug('RPC error: {}'.format(resp['error']))
return resp
if (not 'success' in resp or not resp['success']) and 'message' in resp:
return {'error': resp['message']}
analytics_event('Renew name', total_estimated_cost)
return resp
def cli_revoke(args, config_path=CONFIG_PATH, interactive=True, password=None, proxy=None):
"""
command: revoke
help: Revoke a blockchain ID
arg: name (str) 'The blockchain ID to revoke'
"""
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`.'}
if proxy is None:
proxy = get_default_proxy(config_path)
password = get_default_password(password)
conf = config.get_config(config_path)
assert conf
res = wallet_ensure_exists(config_path=config_path)
if 'error' in res:
return res
fqu = str(args.name)
error = check_valid_name(fqu)
if error:
return {'error': error}
if interactive and os.environ.get("BLOCKSTACK_CLIENT_INTERACTIVE_YES", None) != "1":
try:
input_prompt = (
'WARNING: This will render your name unusable and\n'
'remove any links it points to.\n'
'THIS CANNOT BE UNDONE OR CANCELLED.\n'
'\n'
'Proceed? (y/N) '
)
user_input = raw_input(input_prompt)
user_input = user_input.lower()
if user_input != 'y':
print('Not revoking.')
exit(0)
except KeyboardInterrupt:
print('\nExiting.')
exit(0)
rpc = local_api_connect(config_path=config_path)
assert rpc
log.debug("Revoke {}".format(fqu))
try:
resp = rpc.backend_revoke(fqu)
except Exception as e:
log.exception(e)
return {'error': 'Error talking to server, try again.'}
if 'error' in resp:
log.debug('RPC error: {}'.format(resp['error']))
return resp
if (not 'success' in resp or not resp['success']) and 'message' in resp:
return {'error': resp['message']}
analytics_event('Revoke name', {})
return resp
def cli_migrate(args, config_path=CONFIG_PATH, password=None,
proxy=None, interactive=True, force=False):
"""
command: migrate
help: Migrate a legacy blockchain-linked profile to the latest zonefile and profile format
arg: name (str) 'The blockchain ID with the profile to migrate'
opt: force (str) 'Reset the zone file no matter what.'
"""
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`.'}
password = get_default_password(password)
conf = config.get_config(config_path)
assert conf
res = wallet_ensure_exists(config_path=config_path)
if 'error' in res:
return res
if proxy is None:
proxy = get_default_proxy(config_path=config_path)
fqu = str(args.name)
force = (force or (getattr(args, 'force', '').lower() in ['1', 'yes', 'force', 'true']))
error = check_valid_name(fqu)
if error:
return {'error': error}
# need data public key
_, _, data_pubkey = get_addresses_from_file(config_dir=config_dir)
if data_pubkey is None:
return {'error': 'No data key in wallet'}
res = get_name_zonefile(
fqu, proxy=proxy,
raw_zonefile=True, include_name_record=True
)
user_zonefile = None
user_profile = None
if 'error' not in res:
name_rec = res['name_record']
user_zonefile_txt = res['zonefile']
user_zonefile_hash = get_zonefile_data_hash(user_zonefile_txt)
user_zonefile = None
legacy = False
nonstandard = False
# TODO: handle zone files that do not have data keys.
# try to parse
try:
user_zonefile = blockstack_zones.parse_zone_file(user_zonefile_txt)
legacy = blockstack_profiles.is_profile_in_legacy_format(user_zonefile)
except:
log.warning('Non-standard zonefile {}'.format(user_zonefile_hash))
nonstandard = True
if nonstandard:
if force:
# forcibly reset the zone file
user_profile = make_empty_user_profile()
user_zonefile = make_empty_zonefile(fqu, data_pubkey)
else:
if os.environ.get("BLOCKSTACK_CLIENT_INTERACTIVE_YES", None) != "1":
# prompt
msg = (
''
'WARNING! Non-standard zone file detected.'
'If you proceed, your zone file will be reset.'
''
'Proceed? (y/N): '
)
proceed_str = raw_input(msg)
proceed = proceed_str.lower() in ['y']
if not proceed:
return {'error': 'Non-standard zonefile'}
else:
user_profile = make_empty_user_profile()
user_zonefile = make_empty_zonefile(fqu, data_pubkey)
else:
return {'error': 'Non-standard zonefile'}
# going ahead with zonefile and profile reset
else:
# standard or legacy zone file
if not legacy:
msg = 'Zone file is in the latest format. No migration needed'
return {'error': msg}
# convert
user_profile = blockstack_profiles.get_person_from_legacy_format(user_zonefile)
user_zonefile = make_empty_zonefile(fqu, data_pubkey)
else:
log.error("Failed to get zone file for {}".format(fqu))
return {'error': res['error']}
zonefile_txt = blockstack_zones.make_zone_file(user_zonefile)
zonefile_hash = get_zonefile_data_hash(zonefile_txt)
rpc = local_api_connect(config_path=config_path)
assert rpc
try:
resp = rpc.backend_update(fqu, zonefile_txt, user_profile, None)
except Exception as e:
log.exception(e)
return {'error': 'Error talking to server, try again.'}
if 'error' in resp:
log.debug('RPC error: {}'.format(resp['error']))
return resp
if (not 'success' in resp or not resp['success']) and 'message' in resp:
return {'error': resp['message']}
analytics_event('Migrate name', {})
resp['zonefile_hash'] = zonefile_hash
return resp
def cli_wallet_password(args, config_path=CONFIG_PATH, password=None, interactive=True):
"""
command: wallet_password
help: Change your wallet password
opt: old_password (str) 'The old password. It will be prompted if not given.'
opt: new_password (str) 'The new password. It will be prompted if not given.'
"""
password = get_default_password(password)
if password is None:
password = getattr(args, 'password', None)
wallet_path = get_wallet_path(config_path=config_path)
res = load_wallet(password=password, wallet_path=wallet_path, interactive=interactive, include_private=True)
if 'error' in res:
return res
if res['migrated']:
return {'error': 'Wallet is in legacy format. Please migrate it with `setup_wallet`'}
wallet_keys = res['wallet']
password = res['password']
new_password = getattr(args, 'new_password', None)
if new_password is None:
new_password = prompt_wallet_password('Enter new wallet password: ')
new_password_2 = prompt_wallet_password('Re-enter new wallet password: ')
if new_password != new_password_2:
return {'error': 'New passwords do not match'}
if new_password == password:
return {'error': 'Passwords are the same'}
enc_wallet = encrypt_wallet(wallet_keys, new_password)
if 'error' in enc_wallet:
return enc_wallet
legacy_path = backup_wallet(wallet_path=wallet_path)
if legacy_path is None:
return {'error': 'Failed to replace old wallet'}
res = write_wallet(enc_wallet, path=wallet_path)
if 'error' in res:
return res
try:
os.unlink(legacy_path)
except:
pass
return {'status': True}
def cli_setup_wallet(args, config_path=CONFIG_PATH, password=None, interactive=True):
"""
command: setup_wallet
help: Create or upgrade up your wallet.
"""
password = get_default_password(password)
ret = {}
res = wallet_setup(config_path=config_path, interactive=interactive, password=password)
if 'error' in res:
return res
if res.has_key('backup_wallet'):
# return this
ret['backup_wallet'] = res['backup_wallet']
ret['status'] = True
return ret
def cli_get_public_key(args, config_path=CONFIG_PATH, proxy=None):
"""
command: get_public_key
help: Get the ECDSA public key for a blockchain ID
arg: name (str) 'The blockchain ID'
"""
# reply ENODATA if we can't load the zone file
# reply EINVAL if we can't parse the zone file
fqu = str(args.name)
zfinfo = get_name_zonefile(fqu, proxy=proxy)
if 'error' in zfinfo:
log.error("unable to load zone file for {}: {}".format(fqu, zfinfo['error']))
return {'error': 'unable to load or parse zone file for {}'.format(fqu), 'errno': errno.ENODATA}
if not user_zonefile_data_pubkey(zfinfo['zonefile']):
log.error("zone file for {} has no public key".format(fqu))
return {'error': 'zone file for {} has no public key'.format(fqu), 'errno': errno.EINVAL}
zfpubkey = keylib.key_formatting.decompress(user_zonefile_data_pubkey(zfinfo['zonefile']))
return {'public_key': zfpubkey}
def cli_list_accounts( args, proxy=None, config_path=CONFIG_PATH ):
"""
command: list_accounts advanced
help: List the set of accounts in a name's profile.
arg: name (str) 'The name to query.'
"""
if proxy is None:
proxy = get_default_proxy(config_path=config_path)
name = str(args.name)
account_info = profile_list_accounts(name, proxy=proxy )
if 'error' in account_info:
return account_info
return account_info['accounts']
def cli_get_account( args, proxy=None, config_path=CONFIG_PATH ):
"""
command: get_account advanced
help: Get an account from a name's profile.
arg: name (str) 'The name to query.'
arg: service (str) 'The service for which this account was created.'
arg: identifier (str) 'The name of the account.'
"""
if proxy is None:
proxy = get_default_proxy(config_path=config_path)
name = str(args.name)
service = str(args.service)
identifier = str(args.identifier)
res = profile_get_account(name, service, identifier, config_path=config_path, proxy=proxy)
if 'error' in res:
return {'error': res['error']}
return res['account']
def cli_put_account( args, proxy=None, config_path=CONFIG_PATH, password=None, wallet_keys=None ):
"""
command: put_account advanced
help: Add or overwrite an account in a name's profile.
arg: name (str) 'The name to query.'
arg: service (str) 'The service this account is for.'
arg: identifier (str) 'The name of the account.'
arg: content_url (str) 'The URL that points to external contact data.'
opt: extra_data (str) 'A comma-separated list of "name1=value1,name2=value2,name3=value3..." with any extra account information you need in the account.'
"""
password = get_default_password(password)
proxy = get_default_proxy(config_path=config_path) if proxy is None else proxy
config_dir = os.path.dirname(config_path)
if wallet_keys is None:
wallet_keys = get_wallet_keys(config_path, password)
if 'error' in wallet_keys:
return wallet_keys
name = str(args.name)
service = str(args.service)
identifier = str(args.identifier)
content_url = str(args.content_url)
if not is_name_valid(args.name):
return {'error': 'Invalid name'}
if len(args.service) == 0 or len(args.identifier) == 0 or len(args.content_url) == 0:
return {'error': 'Invalid data'}
# parse extra data
extra_data = {}
if hasattr(args, "extra_data") and args.extra_data is not None:
extra_data_str = str(args.extra_data)
if len(extra_data_str) > 0:
extra_data_pairs = extra_data_str.split(",")
for p in extra_data_pairs:
if '=' not in p:
return {'error': "Could not interpret '%s' in '%s'" % (p, extra_data_str)}
parts = p.split("=")
k = parts[0]
if k in ['service', 'identifier', 'contentUrl']:
continue
v = "=".join(parts[1:])
extra_data[k] = v
return profile_put_account(name, service, identifier, content_url, extra_data, wallet_keys, config_path=config_path, proxy=proxy)
def cli_delete_account( args, proxy=None, config_path=CONFIG_PATH, password=None, wallet_keys=None ):
"""
command: delete_account advanced
help: Delete a particular account from a name's profile.
arg: name (str) 'The name to query.'
arg: service (str) 'The service the account is for.'
arg: identifier (str) 'The identifier of the account to delete.'
"""
password = get_default_password(password)
proxy = get_default_proxy(config_path=config_path) if proxy is None else proxy
config_dir = os.path.dirname(config_path)
if wallet_keys is None:
wallet_keys = get_wallet_keys(config_path, password)
if 'error' in wallet_keys:
return wallet_keys
name = str(args.name)
service = str(args.service)
identifier = str(args.identifier)
if not is_name_valid(args.name):
return {'error': 'Invalid name'}
if len(args.service) == 0 or len(args.identifier) == 0:
return {'error': 'Invalid data'}
return profile_delete_account(name, service, identifier, config_path=config_path, proxy=proxy)
def cli_import_wallet(args, config_path=CONFIG_PATH, password=None, force=False):
"""
command: import_wallet advanced
help: Set the payment, owner, and data private keys for the wallet.
arg: payment_privkey (str) 'Payment private key. M-of-n multisig is supported by passing the CSV string "m,n,pk1,pk2,...".'
arg: owner_privkey (str) 'Name owner private key. M-of-n multisig is supported by passing the CSV string "m,n,pk1,pk2,...".'
arg: data_privkey (str) 'Data-signing private key. Must be a single private key.'
"""
# we require m and n, even though n can be inferred, so we can at least sanity-check the user's arguments.
# it's hard to get both n and the number of private keys wrong in the same way.
config_dir = os.path.dirname(config_path)
wallet_path = os.path.join(config_dir, WALLET_FILENAME)
password = get_default_password(password)
if force and os.path.exists(wallet_path):
# back up
backup_wallet(wallet_path)
if os.path.exists(wallet_path):
msg = 'Back up or remove current wallet first: {}'
return {
'error': 'Wallet already exists!',
'message': msg.format(wallet_path),
}
if password is None:
while True:
res = make_wallet_password(password)
if 'error' in res and password is None:
print(res['error'])
continue
if password is not None:
return res
password = res['password']
break
try:
assert args.owner_privkey
assert args.payment_privkey
assert args.data_privkey
except Exception, e:
if BLOCKSTACK_DEBUG:
log.exception(e)
return {'error': 'Invalid private keys'}
def parse_multisig_csv(multisig_csv):
"""
Helper to parse 'm,n,pk1,pk2.,,,' into a virtualchain private key bundle.
"""
parts = multisig_csv.split(',')
m = None
n = None
try:
m = int(parts[0])
n = int(parts[1])
assert m <= n
assert len(parts[2:]) == n
except ValueError as ve:
log.exception(ve)
log.debug("Invalid multisig CSV {}".format(multisig_csv))
log.error("Invalid m, n")
return {'error': 'Unparseable m or n'}
except AssertionError as ae:
log.exception(ae)
log.debug("Invalid multisig CSV {}".format(multisig_csv))
log.error("Invalid argument: n must not exceed m, and there must be n private keys")
return {'error': 'Invalid argument: invalid values for m or n'}
keys = parts[2:]
key_info = None
try:
key_info = virtualchain.make_multisig_info(m, keys)
except Exception as e:
if BLOCKSTACK_DEBUG:
log.exception(e)
log.error("Failed to make multisig information from keys")
return {'error': 'Failed to make multisig information'}
return key_info
owner_privkey_info = None
payment_privkey_info = None
data_privkey_info = None
# make absolutely certain that these are valid keys or multisig key strings
try:
owner_privkey_info = ecdsa_private_key(str(args.owner_privkey)).to_hex()
except:
log.debug("Owner private key string is not a valid Bitcoin private key")
owner_privkey_info = parse_multisig_csv(args.owner_privkey)
if 'error' in owner_privkey_info:
return owner_privkey_info
try:
payment_privkey_info = ecdsa_private_key(str(args.payment_privkey)).to_hex()
except:
log.debug("Payment private key string is not a valid Bitcoin private key")
payment_privkey_info = parse_multisig_csv(args.payment_privkey)
if 'error' in payment_privkey_info:
return payment_privkey_info
try:
data_privkey_info = ecdsa_private_key(str(args.data_privkey)).to_hex()
except:
log.error("Only single private keys are supported for data at this time")
return {'error': 'Invalid data private key'}
data = make_wallet(password, config_path=config_path,
payment_privkey_info=payment_privkey_info,
owner_privkey_info=owner_privkey_info,
data_privkey_info=data_privkey_info )
if 'error' in data:
return data
write_wallet(data, path=wallet_path)
if not local_api_status(config_dir=config_dir):
return {'status': True}
# load into RPC daemon, if it is running
rpc = local_api_connect(config_path=config_path)
if rpc is None:
log.error("Failed to connect to API endpoint. Trying to shut it down...")
res = local_rpc.local_api_action('stop', config_dir=config_dir)
if 'error' in res:
log.error("Failed to stop API daemon")
return res
return {'status': True}
try:
wallet = decrypt_wallet(data, password, config_path=config_path)
if 'error' in wallet:
return {'error': 'Failed to decrypt new wallet'}
rpc.backend_set_wallet( wallet )
except Exception as e:
log.exception(e)
return {'error': 'Failed to load wallet into API endpoint'}
return {'status': True}
def display_wallet_info(payment_address, owner_address, data_public_key, config_path=CONFIG_PATH):
"""
Print out useful wallet information
"""
print('-' * 60)
print('Payment address:\t{}'.format(payment_address))
print('Owner address:\t\t{}'.format(owner_address))
if data_public_key is not None:
print('Data public key:\t{}'.format(data_public_key))
balance = None
if payment_address is not None:
balance = get_balance( payment_address, config_path=config_path )
if balance is None:
print('Failed to look up balance')
else:
balance = satoshis_to_btc(balance)
print('-' * 60)
print('Balance:')
print('{}: {}'.format(payment_address, balance))
print('-' * 60)
names_owned = None
if owner_address is not None:
names_owned = get_names_owned(owner_address)
if names_owned is None or 'error' in names_owned:
print('Failed to look up names owned')
else:
print('Names Owned:')
names_owned = get_names_owned(owner_address)
print('{}: {}'.format(owner_address, names_owned))
print('-' * 60)
def cli_wallet(args, config_path=CONFIG_PATH, interactive=True, password=None):
"""
command: wallet advanced
help: Query wallet information
"""
password = get_default_password(password)
wallet_path = get_wallet_path(config_path=config_path)
payment_address = None
owner_address = None
data_pubkey = None
migrated = False
config_dir = os.path.dirname(config_path)
if local_api_status(config_dir=config_dir):
wallet_keys = get_wallet_keys(config_path, password)
if 'error' in wallet_keys:
return wallet_keys
result = wallet_keys
payment_address = result['payment_address']
owner_address = result['owner_address']
data_pubkey = result['data_pubkey']
else:
log.debug("API endpoint does not appear to be running")
res = load_wallet(password=password, wallet_path=wallet_path, interactive=interactive, include_private=True)
if 'error' in res:
return res
wallet = res['wallet']
migrated = res['migrated']
payment_address = wallet['payment_addresses'][0]
owner_address = wallet['owner_addresses'][0]
data_pubkey = wallet['data_pubkey']
result = {
'payment_privkey': wallet['payment_privkey'],
'owner_privkey': wallet['owner_privkey'],
'data_privkey': wallet['data_privkey'],
'payment_address': payment_address,
'owner_address': owner_address,
'data_pubkey': data_pubkey
}
payment_privkey = result.get('payment_privkey', None)
owner_privkey = result.get('owner_privkey', None)
data_privkey = result.get('data_privkey', None)
display_wallet_info(
payment_address,
owner_address,
data_pubkey,
config_path=CONFIG_PATH
)
if migrated:
print ('WARNING: Wallet is in legacy format. Please migrate it with `setup_wallet`.')
print('-' * 60)
print('Payment private key info: {}'.format(privkey_to_string(payment_privkey)))
print('Owner private key info: {}'.format(privkey_to_string(owner_privkey)))
print('Data private key info: {}'.format(privkey_to_string(data_privkey)))
print('-' * 60)
return result
def cli_consensus(args, config_path=CONFIG_PATH):
"""
command: consensus advanced
help: Get current consensus information
opt: block_height (int) 'The block height at which to query the consensus information. If not given, the current height is used.'
"""
result = {}
if args.block_height is None:
# by default get last indexed block
resp = getinfo()
if 'error' in resp:
return resp
if 'last_block_processed' in resp and 'consensus' in resp:
return {'consensus': resp['consensus'], 'block_height': resp['last_block_processed']}
else:
log.debug("Resp is {}".format(resp))
return {'error': 'Server is indexing. Try again shortly.'}
resp = get_consensus_at(int(args.block_height))
data = {}
data['consensus'] = resp
data['block_height'] = args.block_height
result = data
return result
def cli_api(args, password=None, interactive=True, config_path=CONFIG_PATH):
"""
command: api
help: Control the RESTful API endpoint
arg: command (str) '"start", "start-foreground", "stop", or "status"'
opt: wallet_password (str) 'The wallet password. Will prompt if required.'
"""
config_dir = CONFIG_DIR
if config_path is not None:
config_dir = os.path.dirname(config_path)
command = str(args.command)
password = get_default_password(password)
if password is None and command in ['start', 'start-foreground']:
password = getattr(args, 'wallet_password', None)
if password is None:
if not interactive:
return {'error': 'No wallet password given, and not in interactive mode'}
password = prompt_wallet_password()
if password:
set_secret("BLOCKSTACK_CLIENT_WALLET_PASSWORD", password)
api_pass = getattr(args, 'api_pass', None)
if api_pass is None:
# environment?
api_pass = get_secret('BLOCKSTACK_API_PASSWORD')
if api_pass is None:
# config file?
conf = config.get_config(config_path)
assert conf
api_pass = conf.get('api_password', None)
set_secret("BLOCKSTACK_API_PASSWORD", api_pass)
if api_pass is None:
return {'error': 'Need --api-password on the CLI, or `api_password=` set in your config file ({})'.format(config_path)}
# sanity check: wallet must exist
if str(args.command) == 'start' and not wallet_exists(config_path=config_path):
return {'error': 'Wallet does not exist. Please create one with `blockstack setup`'}
res = local_rpc.local_api_action(str(args.command), config_dir=config_dir, password=password, api_pass=api_pass)
return res
def cli_name_import(args, interactive=True, config_path=CONFIG_PATH, proxy=None):
"""
command: name_import advanced
help: Import a name to a revealed but not-yet-launched namespace
arg: name (str) 'The name to import'
arg: address (str) 'The address of the name recipient'
arg: zonefile (str) 'The path to the zone file'
arg: privatekey (str) 'An unhardened child private key derived from the namespace reveal key'
"""
import blockstack
proxy = get_default_proxy() if proxy is None else proxy
config_dir = os.path.dirname(config_path)
conf = config.get_config(config_path)
assert conf
assert 'queue_path' in conf
queue_path = conf['queue_path']
# NOTE: we only need this for the queues. This command is otherwise not accessible from the RESTful API,
if not local_api_status(config_dir=config_dir):
return {'error': 'API server not running. Please start it with `blockstack api start`.'}
name = str(args.name)
address = virtualchain.address_reencode( str(args.address) )
zonefile_path = str(args.zonefile_path)
privkey = str(args.privatekey)
try:
privkey = ecdsa_private_key(privkey).to_hex()
except:
return {'error': 'Invalid private key'}
if not re.match(OP_BASE58CHECK_PATTERN, address):
return {'error': 'Invalid address'}
if not os.path.exists(zonefile_path):
return {'error': 'No such file or directory: {}'.format(zonefile_path)}
zonefile_data = None
try:
with open(zonefile_path) as f:
zonefile_data = f.read()
except Exception as e:
if BLOCKSTACK_DEBUG:
log.exception(e)
return {'error': 'Failed to load {}'.format(zonefile_path)}
zonefile_info = analyze_zonefile_string(name, zonefile_data, force_data=True, check_current=False, proxy=proxy)
if 'error' in zonefile_info:
log.error("Failed to analyze zone file: {}".format(zonefile_info['error']))
return {'error': zonefile_info['error']}
if zonefile_info['nonstandard']:
if interactive:
proceed = prompt_invalid_zonefile()
if not proceed:
return {'error': 'Aborting name import on invalid zone file'}
else:
log.warning("Using nonstandard zone file data")
zonefile_data = zonefile_info['zonefile_str']
zonefile_hash = get_zonefile_data_hash(zonefile_data)
if interactive:
fees = get_price_and_fees(name, ['name_import'], privkey, privkey, config_path=config_path, proxy=proxy)
if 'error' in fees:
return fees
print("Import cost breakdown:\n{}".format(json.dumps(fees, indent=4, sort_keys=True)))
print("Importing name '{}' to be owned by '{}' with zone file hash '{}'".format(name, address, zonefile_hash))
proceed = raw_input("Proceed? (y/N) ")
if proceed.lower() != 'y':
return {'error': 'Name import aborted'}
utxo_client = get_utxo_provider_client(config_path=config_path)
tx_broadcaster = get_tx_broadcaster(config_path=config_path)
res = do_name_import(name, privkey, address, zonefile_hash, utxo_client, tx_broadcaster, config_path=config_path, proxy=proxy, safety_checks=True)
if 'error' in res:
return res
# success! enqueue to back-end
queue_append("name_import", name, res['transaction_hash'], zonefile_data=zonefile_data, zonefile_hash=zonefile_hash, owner_address=address, config_path=config_path, path=queue_path)
res['value_hash'] = zonefile_hash
return res
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: 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)
nsid = str(args.namespace_id)
ns_privkey = str(args.payment_privkey)
ns_reveal_privkey = str(args.reveal_privkey)
reveal_addr = None
try:
ns_privkey = keylib.ECPrivateKey(ns_privkey).to_hex()
ns_reveal_privkey = keylib.ECPrivateKey(ns_reveal_privkey).to_hex()
# force compresssed
ns_privkey = set_privkey_compressed(ns_privkey, compressed=True)
ns_reveal_privkey = set_privkey_compressed(ns_reveal_privkey, compressed=True)
keylib.ECPrivateKey(ns_privkey)
reveal_addr = virtualchain.get_privkey_address(ns_reveal_privkey)
except Exception as e:
if BLOCKSTACK_DEBUG:
log.exception(e)
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 or 'warnings' in fees:
return fees
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']),
'The address {} will be used to pay the fee'.format(virtualchain.get_privkey_address(ns_privkey)),
'The address {} will be used to reveal the namespace.'.format(reveal_addr),
'MAKE SURE YOU KEEP THE PRIVATE KEYS FOR BOTH ADDRESSES\n',
"\n",
"Before you preorder this namespace, 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) "
]
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 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 interactively set its pricing parameters
arg: namespace_id (str) 'The namespace ID'
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
res = is_namespace_valid(namespace_id)
if not res:
return {'error': 'Invalid namespace ID'}
try:
privkey = keylib.ECPrivateKey(privkey).to_hex()
reveal_privkey = keylib.ECPrivateKey(reveal_privkey).to_hex()
# force compresssed
ns_privkey = set_privkey_compressed(privkey, compressed=True)
ns_reveal_privkey = set_privkey_compressed(reveal_privkey, compressed=True)
ecdsa_private_key(privkey)
reveal_addr = virtualchain.get_privkey_address(reveal_privkey)
except Exception as e:
if BLOCKSTACK_DEBUG:
log.exception(e)
return {'error': 'Invalid private keys'}
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:
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)
return {'error': 'Invalid namespace parameters'}
# 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
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.get_privkey_address(privkey)))
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, 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: reveal_privkey (str) 'The private key used to import names'
"""
import blockstack
namespace_id = str(args.namespace_id)
reveal_privkey = str(args.reveal_privkey)
res = is_namespace_valid(namespace_id)
if not res:
return {'error': 'Invalid namespace ID'}
try:
reveal_privkey = keylib.ECPrivateKey(reveal_privkey).to_hex()
# force compresssed
reveal_privkey = set_privkey_compressed(reveal_privkey, compressed=True)
reveal_addr = virtualchain.get_privkey_address(reveal_privkey)
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, storage_drivers=None, storage_drivers_exclusive=False):
"""
command: put_mutable advanced
help: Low-level method to store off-chain signed data.
arg: name (str) 'The name that points to the zone file to use'
arg: data_id (str) 'The name of the data'
arg: data_path (str) 'The path to the data to store'
opt: privkey (str) 'The private key to sign with'
opt: version (str) 'The version of this data to store'
"""
password = get_default_password(password)
fqu = str(args.name)
data_id = str(args.data_id)
data_path = str(args.data)
data = None
with open(data_path, 'r') as f:
data = f.read()
error = check_valid_name(fqu)
if error:
return {'error': error}
config_dir = os.path.dirname(config_path)
privkey = None
if not hasattr(args, 'privkey') or args.privkey is None:
# get the data private key from the wallet.
# put_mutable() should only succeed if the zone file for this name
# has the corresponding public key, since otherwise there would be
# no way to authenticate this data.
zfinfo = get_name_zonefile(fqu, proxy=proxy)
if 'error' in zfinfo:
log.error("unable to load zone file for {}: {}".format(fqu, zfinfo['error']))
return {'error': 'unable to load or parse zone file for {}'.format(fqu)}
if not user_zonefile_data_pubkey(zfinfo['zonefile']):
log.error("zone file for {} has no public key".format(fqu))
return {'error': 'zone file for {} has no public key'.format(fqu)}
wallet_keys = get_wallet_keys(config_path, password)
if 'error' in wallet_keys:
return wallet_keys
privkey = str(wallet_keys['data_privkey'])
pubkey = keylib.key_formatting.compress(ecdsa_private_key(privkey).public_key().to_hex())
zfpubkey = keylib.key_formatting.compress(user_zonefile_data_pubkey(zfinfo['zonefile']))
if pubkey != zfpubkey:
log.error("public key mismatch: wallet public key {} != zonefile public key {}".format(pubkey, zfpubkey))
return {'error': 'public key mismatch: wallet public key {} does not match zone file public key {}'.format(pubkey, zfpubkey)}
else:
privkey = str(args.privkey)
try:
pubkey = ECPrivateKey(privkey).public_key().to_hex()
except:
if BLOCKSTACK_TEST:
log.error("Invalid private key {}".format(privkey))
return {'error': 'Failed to parse private key'}
mutable_data_info = make_mutable_data_info(data_id, data, blockchain_id=fqu, config_path=config_path)
mutable_data_payload = data_blob_serialize(mutable_data_info)
proxy = get_default_proxy(config_path=config_path) if proxy is None else proxy
sig = sign_data_payload(mutable_data_payload, privkey)
result = put_mutable(mutable_data_info['fq_data_id'], mutable_data_payload, pubkey, sig, mutable_data_info['version'], \
blockchain_id=fqu, config_path=config_path, proxy=proxy, storage_drivers=storage_drivers, storage_drivers_exclusive=storage_drivers_exclusive)
if 'error' in result:
return result
return result
def cli_put_immutable(args, config_path=CONFIG_PATH, password=None, proxy=None):
"""
command: put_immutable advanced
help: Put signed, blockchain-hashed data into your storage providers.
arg: name (str) 'The name that points to the zone file to use'
arg: data_id (str) 'The name of the data'
arg: data (str) 'Path to the data to store'
"""
password = get_default_password(password)
config_dir = os.path.dirname(config_path)
fqu = str(args.name)
error = check_valid_name(fqu)
if error:
return {'error': error}
data_path = str(args.data)
with open(data_path, 'r') as f:
data = f.read()
wallet_keys = get_wallet_keys(config_path, password)
if 'error' in wallet_keys:
return wallet_keys
proxy = get_default_proxy() if proxy is None else proxy
result = put_immutable(
fqu, str(args.data_id), data,
wallet_keys=wallet_keys, proxy=proxy
)
if 'error' in result:
return result
data_hash = result['immutable_data_hash']
result['hash'] = data_hash
return result
def cli_get_mutable(args, config_path=CONFIG_PATH, proxy=None):
"""
command: get_mutable advanced
help: Low-level method to get signed off-chain data.
arg: name (str) 'The blockchain ID that owns the data'
arg: data_id (str) 'The name of the data'
opt: data_pubkey (str) 'The public key to use to verify the data'
opt: device_ids (str) 'A CSV of devices to query'
"""
data_pubkey = str(args.data_pubkey) if hasattr(args, 'data_pubkey') and getattr(args, 'data_pubkey') is not None else None
# get the list of device IDs to use
device_ids = getattr(args, 'device_ids', None)
if device_ids:
device_ids = device_ids.split(',')
else:
raise NotImplemented("Missing token file parsing logic")
result = get_mutable(str(args.data_id), device_ids['device_ids'], proxy=proxy, config_path=config_path, blockchain_id=str(args.name), data_pubkey=data_pubkey)
if 'error' in result:
return result
return {'status': True, 'data': result['data']}
def cli_get_immutable(args, config_path=CONFIG_PATH, proxy=None):
"""
command: get_immutable advanced
help: Get signed, blockchain-hashed data from storage providers.
arg: name (str) 'The name that points to the zone file with the data hash'
arg: data_id_or_hash (str) 'Either the name or the SHA256 of the data to obtain'
"""
proxy = get_default_proxy() if proxy is None else proxy
if is_valid_hash( args.data_id_or_hash ):
result = get_immutable(str(args.name), str(args.data_id_or_hash), proxy=proxy)
if 'error' not in result:
return result
# either not a valid hash, or no such data with this hash.
# maybe this hash-like string is the name of something?
result = get_immutable_by_name(str(args.name), str(args.data_id_or_hash), proxy=proxy)
if 'error' in result:
return result
return {
'data': result['data'],
'hash': result['hash']
}
def cli_list_update_history(args, config_path=CONFIG_PATH):
"""
command: list_update_history advanced
help: List the history of update hashes for a name
arg: name (str) 'The name whose data to list'
"""
result = list_update_history(str(args.name))
return result
def cli_list_zonefile_history(args, config_path=CONFIG_PATH):
"""
command: list_zonefile_history advanced
help: List the history of zonefiles for a name (if they can be obtained)
arg: name (str) 'The name whose zonefiles to list'
"""
result = list_zonefile_history(str(args.name))
return result
def cli_list_immutable_data_history(args, config_path=CONFIG_PATH):
"""
command: list_immutable_data_history advanced
help: List all prior hashes of a given immutable datum
arg: name (str) 'The name whose data to list'
arg: data_id (str) 'The data identifier whose history to list'
"""
result = list_immutable_data_history(str(args.name), str(args.data_id))
return result
def cli_delete_immutable(args, config_path=CONFIG_PATH, proxy=None, password=None):
"""
command: delete_immutable advanced
help: Delete an immutable datum from a zonefile.
arg: name (str) 'The name that owns the data'
arg: data_id (str) 'The SHA256 of the data to remove, or the data ID'
"""
password = get_default_password(password)
config_dir = os.path.dirname(config_path)
fqu = str(args.name)
error = check_valid_name(fqu)
if error:
return {'error': error}
wallet_keys = get_wallet_keys(config_path, password)
if 'error' in wallet_keys:
return wallet_keys
if proxy is None:
proxy = get_default_proxy()
result = None
if is_valid_hash(str(args.data_id)):
result = delete_immutable(
str(args.name), str(args.data_id),
proxy=proxy, wallet_keys=wallet_keys
)
else:
result = delete_immutable(
str(args.name), None, data_id=str(args.data_id),
proxy=proxy, wallet_keys=wallet_keys
)
return result
def cli_delete_mutable(args, config_path=CONFIG_PATH, password=None, proxy=None):
"""
command: delete_mutable advanced
help: Low-level method to delete signed off-chain data.
arg: name (str) 'The name that owns the data'
arg: data_id (str) 'The ID of the data to remove'
opt: privkey (str) 'If given, the data private key to use'
"""
password = get_default_password(password)
data_id = str(args.data_id)
fqu = str(args.name)
error = check_valid_name(fqu)
if error:
return {'error': error}
config_dir = os.path.dirname(config_path)
# this should only succeed if the zone file is well-formed,
# since otherwise no one would be able to get the public key
# to verify the tombstones.
zfinfo = get_name_zonefile(fqu, proxy=proxy)
if 'error' in zfinfo:
log.error("Unable to load zone file for {}: {}".format(fqu, zfinfo['error']))
return {'error': 'Unable to load or parse zone file for {}'.format(fqu)}
if not user_zonefile_data_pubkey(zfinfo['zonefile']):
log.error("Zone file for {} has no public key".format(fqu))
return {'error': 'Zone file for {} has no public key'.format(fqu)}
privkey = None
if hasattr(args, 'privkey') and args.privkey:
privkey = str(args.privkey)
else:
wallet_keys = get_wallet_keys(config_path, password)
if 'error' in wallet_keys:
return wallet_keys
privkey = wallet_keys['data_privkey']
assert privkey
proxy = get_default_proxy(config_path=config_path) if proxy is None else proxy
data_tombstones = make_mutable_data_tombstones(device_ids, data_id)
signed_data_tombstones = sign_mutable_data_tombstones(data_tombstones, privkey)
result = delete_mutable(data_id, signed_data_tombstones, proxy=proxy, config_path=config_path)
return result
def cli_get_name_blockchain_record(args, config_path=CONFIG_PATH):
"""
command: get_name_blockchain_record advanced
help: Get the raw blockchain record for a name
arg: name (str) 'The name to list'
"""
result = get_name_blockchain_record(str(args.name))
return result
def cli_get_name_blockchain_history(args, config_path=CONFIG_PATH):
"""
command: get_name_blockchain_history advanced
help: Get a sequence of historic blockchain records for a name
arg: name (str) 'The name to query'
opt: start_block (int) 'The start block height'
opt: end_block (int) 'The end block height'
"""
start_block = args.start_block
if start_block is None:
start_block = FIRST_BLOCK_MAINNET
else:
start_block = int(args.start_block)
end_block = args.end_block
if end_block is None:
# I would love to have to update this number in the future,
# if it proves too small. That would be a great problem
# to have :-)
end_block = 100000000
else:
end_block = int(args.end_block)
result = get_name_blockchain_history(str(args.name), start_block, end_block)
return result
def cli_get_namespace_blockchain_record(args, config_path=CONFIG_PATH):
"""
command: get_namespace_blockchain_record advanced
help: Get the raw namespace blockchain record for a name
arg: namespace_id (str) 'The namespace ID to list'
"""
result = get_namespace_blockchain_record(str(args.namespace_id))
return result
def cli_lookup_snv(args, config_path=CONFIG_PATH):
"""
command: lookup_snv advanced
help: Use SNV to look up a name at a particular block height
arg: name (str) 'The name to query'
arg: block_id (int) 'The block height at which to query the name'
arg: trust_anchor (str) 'The trusted consensus hash, transaction ID, or serial number from a higher block height than `block_id`'
"""
result = lookup_snv(
str(args.name),
int(args.block_id),
str(args.trust_anchor)
)
return result
def cli_get_name_zonefile(args, config_path=CONFIG_PATH, raw=True):
"""
command: get_name_zonefile advanced raw
help: Get a name's zonefile
arg: name (str) 'The name to query'
opt: json (str) 'If true is given, try to parse as JSON'
"""
# the 'raw' kwarg is set by the API daemon to False to get back structured data
parse_json = getattr(args, 'json', 'false')
parse_json = parse_json is not None and parse_json.lower() in ['true', '1']
result = get_name_zonefile(str(args.name), raw_zonefile=True)
if 'error' in result:
log.error("get_name_zonefile failed: %s" % result['error'])
return result
if 'zonefile' not in result:
return {'error': 'No zonefile data'}
if parse_json:
# try to parse
try:
new_zonefile = decode_name_zonefile(name, result['zonefile'])
assert new_zonefile is not None
result['zonefile'] = new_zonefile
except:
result['warning'] = 'Non-standard zonefile'
if raw:
return result['zonefile']
else:
return result
def cli_get_names_owned_by_address(args, config_path=CONFIG_PATH):
"""
command: get_names_owned_by_address advanced
help: Get the list of names owned by an address
arg: address (str) 'The address to query'
"""
result = get_names_owned_by_address(str(args.address))
return result
def cli_get_namespace_cost(args, config_path=CONFIG_PATH):
"""
command: get_namespace_cost advanced
help: Get the cost of a namespace
arg: namespace_id (str) 'The namespace ID to query'
"""
result = get_namespace_cost(str(args.namespace_id))
return result
def get_offset_count(offset, count):
return (
int(offset) if offset is not None else -1,
int(count) if count is not None else -1,
)
def cli_get_all_names(args, config_path=CONFIG_PATH):
"""
command: get_all_names advanced
help: Get all names in existence, optionally paginating through them
arg: page (int) 'The page of names to fetch (groups of 100)'
"""
offset = int(args.page) * 100
count = 100
result = get_all_names(offset=offset, count=count)
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 advanced
help: Get the names in a given namespace, optionally paginating through them
arg: namespace_id (str) 'The ID of the namespace to query'
arg: page (int) 'The page of names to fetch (groups of 100)'
"""
offset = int(args.page) * 100
count = 100
result = get_names_in_namespace(str(args.namespace_id), offset, count)
return result
def cli_get_nameops_at(args, config_path=CONFIG_PATH):
"""
command: get_nameops_at advanced
help: Get the list of name operations that occurred at a given block number
arg: block_id (int) 'The block height to query'
"""
result = get_nameops_at(int(args.block_id))
return result
def cli_set_zonefile_hash(args, config_path=CONFIG_PATH, password=None):
"""
command: set_zonefile_hash advanced
help: Directly set the hash associated with the name in the blockchain.
arg: name (str) 'The name to update'
arg: zonefile_hash (str) 'The RIPEMD160(SHA256(zonefile)) hash'
"""
password = get_default_password(password)
conf = config.get_config(config_path)
assert conf
config_dir = os.path.dirname(config_path)
fqu = str(args.name)
error = check_valid_name(fqu)
if error:
return {'error': error}
zonefile_hash = str(args.zonefile_hash)
if re.match(r'^[a-fA-F0-9]+$', zonefile_hash) is None or len(zonefile_hash) != 40:
return {'error': 'Not a valid zonefile hash'}
# forward along to RESTful server (or registrar)
log.debug("Update {}, zonefile_hash={}".format(fqu, zonefile_hash))
rpc = local_api_connect(config_path=config_path)
assert rpc
try:
resp = rpc.backend_update(fqu, None, None, zonefile_hash )
except Exception as e:
log.exception(e)
return {'error': 'Error talking to server, try again.'}
if 'error' in resp:
log.debug('RPC error: {}'.format(resp['error']))
return resp
if (not 'success' in resp or not resp['success']) and 'message' in resp:
return {'error': resp['message']}
analytics_event('Set zonefile hash', {})
resp['zonefile_hash'] = zonefile_hash
return resp
def cli_unqueue(args, config_path=CONFIG_PATH):
"""
command: unqueue advanced
help: Remove a stuck transaction from the queue.
arg: name (str) 'The affected name'
arg: queue_id (str) 'The type of queue ("preorder", "register", "update", etc)'
arg: txid (str) 'The transaction ID'
"""
conf = config.get_config(config_path)
queue_path = conf['queue_path']
try:
queuedb_remove(
str(args.queue_id), str(args.name),
str(args.txid), path=queue_path
)
except:
msg = 'Failed to remove from queue\n{}'
return {'error': msg.format(traceback.format_exc())}
return {'status': True}
def cli_put_profile(args, config_path=CONFIG_PATH, password=None, proxy=None, force_data=False, wallet_keys=None):
"""
command: put_profile advanced
help: Set the profile for a blockchain ID.
arg: blockchain_id (str) 'The blockchain ID.'
arg: data (str) 'The profile as a JSON string, or a path to the profile.'
"""
password = get_default_password(password)
config_dir = os.path.dirname(config_path)
conf = config.get_config(config_path)
name = str(args.blockchain_id)
profile_json_str = str(args.data)
proxy = get_default_proxy() if proxy is None else proxy
profile = None
if not force_data and is_valid_path(profile_json_str) and os.path.exists(profile_json_str):
# this is a path. try to load it
try:
with open(profile_json_str, 'r') as f:
profile_json_str = f.read()
except:
return {'error': 'Failed to load "{}"'.format(profile_json_str)}
# try to parse it
try:
profile = json.loads(profile_json_str)
except:
return {'error': 'Invalid profile JSON'}
if wallet_keys is None:
wallet_keys = get_wallet_keys(config_path, password)
if 'error' in wallet_keys:
return wallet_keys
required_storage_drivers = conf.get(
'storage_drivers_required_write',
config.BLOCKSTACK_REQUIRED_STORAGE_DRIVERS_WRITE
)
required_storage_drivers = required_storage_drivers.split()
user_zonefile = get_name_zonefile(name, proxy=proxy)
if 'error' in user_zonefile:
return user_zonefile
user_zonefile = user_zonefile['zonefile']
if blockstack_profiles.is_profile_in_legacy_format(user_zonefile):
msg = 'Profile in legacy format. Please migrate it with the "migrate" command first.'
return {'error': msg}
res = put_profile(name, profile, user_zonefile=user_zonefile,
wallet_keys=wallet_keys, proxy=proxy,
required_drivers=required_storage_drivers, blockchain_id=name,
config_path=config_path)
if 'error' in res:
return res
return {'status': True}
def cli_delete_profile(args, config_path=CONFIG_PATH, password=None, proxy=None, wallet_keys=None ):
"""
command: delete_profile advanced
help: Delete a profile from a blockchain ID.
arg: blockchain_id (str) 'The blockchain ID.'
"""
proxy = get_default_proxy() if proxy is None else proxy
password = get_default_password(password)
name = str(args.blockchain_id)
if wallet_keys is None:
wallet_keys = get_wallet_keys(config_path, password)
if 'error' in wallet_keys:
return wallet_keys
res = delete_profile(name, user_data_privkey=wallet_keys['data_privkey'], proxy=proxy, wallet_keys=wallet_keys)
return res
def cli_sync_zonefile(args, config_path=CONFIG_PATH, proxy=None, interactive=True, nonstandard=False):
"""
command: sync_zonefile advanced
help: Upload the current zone file to all storage providers.
arg: name (str) 'Name of the zone file to synchronize.'
opt: txid (str) 'NAME_UPDATE transaction ID that set the zone file.'
opt: zonefile (str) 'The path to the zone file on disk, if unavailable from other sources.'
opt: nonstandard (str) 'If true, do not attempt to parse the zonefile. Just upload as-is.'
"""
conf = config.get_config(config_path)
assert 'server' in conf
assert 'port' in conf
assert 'queue_path' in conf
queue_path = conf['queue_path']
name = str(args.name)
proxy = get_default_proxy(config_path=config_path) if proxy is None else proxy
txid = None
if hasattr(args, 'txid'):
txid = getattr(args, 'txid')
user_data, zonefile_hash = None, None
if not nonstandard and getattr(args, 'nonstandard', None):
nonstandard = args.nonstandard.lower() in ['yes', '1', 'true']
if getattr(args, 'zonefile', None) is not None:
# zonefile path given
zonefile_path = str(args.zonefile)
zonefile_info = analyze_zonefile_string(name, zonefile_path, proxy=proxy)
if 'error' in zonefile_info:
log.error("Failed to analyze user zonefile: {}".format(zonefile_info['error']))
return {'error': zonefile_info['error']}
if zonefile_info.get('nonstandard'):
log.warning("Non-standard zone file")
if interactive and not nonstandard:
proceed = prompt_invalid_zonefile()
if not proceed:
return {'error': 'Non-standard zone file'}
user_data = zonefile_info['zonefile_str']
if txid is None or user_data is None:
# load zonefile and txid from queue?
queued_data = queuedb_find('update', name, path=queue_path)
if queued_data:
# find the current one (get raw zonefile)
log.debug("%s updates queued for %s" % (len(queued_data), name))
for queued_zfdata in queued_data:
update_data = queue_extract_entry(queued_zfdata)
zfdata = update_data.get('zonefile', None)
if zfdata is None:
continue
user_data = zfdata
txid = queued_zfdata.get('tx_hash', None)
break
if user_data is None:
# not in queue. Maybe it's available from one of the storage drivers?
log.debug('no pending updates for "{}"; try storage'.format(name))
user_data = get_name_zonefile( name, raw_zonefile=True )
if 'error' in user_data:
msg = 'Failed to get zonefile: {}'
log.error(msg.format(user_data['error']))
return user_data
user_data = user_data['zonefile']
# have user data
zonefile_hash = storage.get_zonefile_data_hash(user_data)
if txid is None:
# not in queue. Fetch from blockstack server
name_rec = get_name_blockchain_record(name, proxy=proxy)
if 'error' in name_rec:
msg = 'Failed to get name record for {}: {}'
log.error(msg.format(name, name_rec['error']))
msg = 'Failed to get name record to look up tx hash.'
return {'error': msg}
# find the tx hash that corresponds to this zonefile
if name_rec['op'] == NAME_UPDATE:
if name_rec['value_hash'] == zonefile_hash:
txid = name_rec['txid']
else:
name_history = name_rec['history']
for history_key in reversed(sorted(name_history)):
name_history_item = name_history[history_key]
op = name_history_item.get('op', None)
if op is None:
continue
if op != NAME_UPDATE:
continue
value_hash = name_history_item.get('value_hash', None)
if value_hash is None:
continue
if value_hash != zonefile_hash:
continue
txid = name_history_item.get('txid', None)
break
if txid is None:
log.error('Unable to lookup txid for update {}, {}'.format(name, zonefile_hash))
return {'error': 'Unable to lookup txid that wrote zonefile'}
# can proceed to replicate
res = zonefile_data_replicate(
name, user_data, txid,
[(conf['server'], conf['port'])],
config_path=config_path
)
if 'error' in res:
log.error('Failed to replicate zonefile: {}'.format(res['error']))
return res
return {'status': True, 'zonefile_hash': zonefile_hash}
def cli_convert_legacy_profile(args, config_path=CONFIG_PATH):
"""
command: convert_legacy_profile advanced
help: Convert a legacy profile into a modern profile.
arg: path (str) 'Path on disk to the JSON file that contains the legacy profile data from Onename'
"""
profile_json_str, profile = None, None
try:
with open(args.path, 'r') as f:
profile_json_str = f.read()
profile = json.loads(profile_json_str)
except:
return {'error': 'Failed to load profile JSON'}
# should have 'profile' key
if 'profile' not in profile:
return {'error': 'JSON has no "profile" key'}
profile = profile['profile']
profile = blockstack_profiles.get_person_from_legacy_format(profile)
return profile
def get_app_name(appname):
"""
Get the application name, or if not given, the default name
"""
return appname if appname is not None else '_default'
def cli_app_publish( args, config_path=CONFIG_PATH, interactive=False, password=None, proxy=None ):
"""
command: app_publish advanced
help: Publish a Blockstack application
arg: blockchain_id (str) 'The blockchain ID that will own the application'
arg: app_domain (str) 'The application domain name'
arg: methods (str) 'A comma-separated list of API methods this application will call'
arg: index_file (str) 'The path to the index file'
opt: urls (str) 'A comma-separated list of URLs to publish the index file to'
opt: drivers (str) 'A comma-separated list of storage drivers for clients to use'
"""
password = get_default_password(password)
blockchain_id = str(args.blockchain_id)
app_domain = str(args.app_domain)
config_dir = os.path.dirname(config_path)
if proxy is None:
proxy = get_default_proxy(config_path)
index_file_data = None
try:
with open(args.index_file, 'r') as f:
index_file_data = f.read()
except:
return {'error': 'Failed to load index file'}
methods = None
if hasattr(args, 'methods') and args.methods is not None:
methods = str(args.methods).split(',')
# TODO: validate
else:
methods = []
drivers = []
if hasattr(args, 'drivers') and args.drivers is not None:
drivers = str(args.drivers).split(",")
uris = []
index_data_id = '{}/index.html'.format(app_domain)
if not hasattr(args, 'urls') or args.urls is not None:
urls = str(args.urls).split(',')
else:
urls = get_driver_urls( index_data_id, get_storage_handlers() )
uris = [url_to_uri_record(u, datum_name=index_data_id) for u in urls]
wallet_keys = get_wallet_keys(config_path, password)
if 'error' in wallet_keys:
return wallet_keys
res = app_publish( blockchain_id, app_domain, methods, uris, index_file_data, app_driver_hints=drivers, wallet_keys=wallet_keys, proxy=proxy, config_path=config_path )
if 'error' in res:
return res
return {'status': True}
def cli_app_get_config( args, config_path=CONFIG_PATH, interactive=False, proxy=None ):
"""
command: app_get_config advanced
help: Get the configuration structure for an application.
arg: blockchain_id (str) 'The app owner blockchain ID'
arg: app_domain (str) 'The application domain name'
"""
if proxy is None:
proxy = get_default_proxy(config_path)
blockchain_id = str(args.blockchain_id)
app_domain = str(args.app_domain)
app_config = app_get_config(blockchain_id, app_domain, proxy=proxy, config_path=config_path )
return app_config
def cli_app_get_resource( args, config_path=CONFIG_PATH, interactive=False, proxy=None ):
"""
command: app_get_resource advanced
help: Get an application resource from mutable storage.
arg: blockchain_id (str) 'The app owner blockchain ID'
arg: app_domain (str) 'The application domain name'
arg: res_path (str) 'The resource path'
opt: pubkey (str) 'The public key'
"""
if proxy is None:
proxy = get_default_proxy(config_path)
blockchain_id = str(args.blockchain_id)
app_domain = str(args.app_domain)
res_path = str(args.res_path)
pubkey = None
if hasattr(args, "pubkey") and args.pubkey:
pubkey = str(args.pubkey)
res = app_get_resource( blockchain_id, app_domain, res_path, data_pubkey=pubkey, proxy=proxy, config_path=config_path )
return res
def cli_app_put_resource( args, config_path=CONFIG_PATH, interactive=False, proxy=None, password=None ):
"""
command: app_put_resource advanced
help: Store an application resource from mutable storage.
arg: blockchain_id (str) 'The app owner blockchain ID'
arg: app_domain (str) 'The application domain name'
arg: res_path (str) 'The location to which to store this resource'
arg: res_file (str) 'The path on disk to the resource to upload'
"""
if proxy is None:
proxy = get_default_proxy(config_path)
password = get_default_password(password)
blockchain_id = str(args.blockchain_id)
app_domain = str(args.app_domain)
res_path = str(args.res_path)
res_file_path = str(args.res_file)
resdata = None
if not os.path.exists(res_file_path):
return {'error': 'No such file or directory'}
with open(res_file_path, "r") as f:
resdata = f.read()
config_dir = os.path.dirname(config_path)
wallet_keys = get_wallet_keys(config_path, password)
if 'error' in wallet_keys:
return wallet_keys
res = app_put_resource( blockchain_id, app_domain, res_path, resdata, proxy=proxy, wallet_keys=wallet_keys, config_path=config_path )
return res
def cli_app_delete_resource( args, config_path=CONFIG_PATH, interactive=False, proxy=None, password=None ):
"""
command: app_delete_resource advanced
help: Delete an application resource from mutable storage.
arg: blockchain_id (str) 'The app owner blockchain ID'
arg: app_domain (str) 'The application domain name'
arg: res_path (str) 'The location to which to store this resource'
"""
if proxy is None:
proxy = get_default_proxy(config_path)
password = get_default_password(password)
blockchain_id = str(args.blockchain_id)
app_domain = str(args.app_domain)
res_path = str(args.res_path)
config_dir = os.path.dirname(config_path)
wallet_keys = get_wallet_keys(config_path, password)
if 'error' in wallet_keys:
return wallet_keys
res = app_delete_resource( blockchain_id, app_domain, res_path, proxy=proxy, wallet_keys=wallet_keys, config_path=config_path )
return res
def cli_app_signin(args, config_path=CONFIG_PATH, interactive=True):
"""
command: app_signin advanced
help: Create a session token for the RESTful API for a given application
arg: blockchain_id (str) 'The blockchain ID of the caller'
arg: privkey (str) 'The app-specific private key to use'
arg: app_domain (str) 'The application domain'
arg: api_methods (str) 'A CSV of requested methods to allow'
arg: device_ids (str) 'A CSV of device IDs that can write to the app datastore'
arg: public_keys (str) 'A CSV of public keys that can write to the app datastore'
"""
blockchain_id = str(args.blockchain_id)
app_domain = str(args.app_domain)
api_methods = str(args.api_methods)
app_privkey = str(args.privkey)
device_ids = str(args.device_ids)
public_keys = str(args.public_keys)
api_methods = api_methods.split(',')
device_ids = device_ids.split(',')
public_keys = public_keys.split(',')
if len(device_ids) != len(public_keys):
return {'error': 'Mismatch between device IDs and public keys'}
for pubk in public_keys:
try:
keylib.ECPublicKey(pubk)
except:
return {'error': 'Invalid public key {}'.format(pubk)}
# get API password
api_pass = get_secret("BLOCKSTACK_API_PASSWORD")
if api_pass is None:
conf = config.get_config(config_path)
if conf:
api_pass = conf.get('api_password', None)
if api_pass is None:
if interactive:
try:
api_pass = getpass.getpass("API password: ")
except KeyboardInterrupt:
return {'error': 'Keyboard interrupt'}
else:
return {'error': 'No API password set'}
# TODO: validate API methods
# TODO: fetch api methods from app domain, if need be
this_device_id = config.get_local_device_id(config_dir=os.path.dirname(config_path))
rpc = local_api_connect(config_path=config_path, api_pass=api_pass)
sesinfo = rpc.backend_signin(blockchain_id, app_privkey, app_domain, api_methods, device_ids, public_keys, this_device_id)
if 'error' in sesinfo:
return sesinfo
return {'status': True, 'token': sesinfo['token']}
def cli_sign_profile( args, config_path=CONFIG_PATH, proxy=None, password=None, interactive=False ):
"""
command: sign_profile advanced raw
help: Sign a JSON file to be used as a profile.
arg: path (str) 'The path to the profile data on disk.'
opt: privkey (str) 'The optional private key to sign it with (defaults to the data private key in your wallet)'
"""
if proxy is None:
proxy = get_default_proxy(config_path=config_path)
password = get_default_password(password)
config_dir = os.path.dirname(config_path)
path = str(args.path)
data_json = None
try:
with open(path, 'r') as f:
dat = f.read()
data_json = json.loads(dat)
except Exception as e:
if os.environ.get("BLOCKSTACK_DEBUG") == "1":
log.exception(e)
log.error("Failed to load {}".format(path))
return {'error': 'Failed to load {}'.format(path)}
privkey = None
if hasattr(args, "privkey") and args.privkey:
privkey = str(args.privkey)
else:
wallet_keys = get_wallet_keys( config_path, password )
if 'error' in wallet_keys:
return wallet_keys
if not wallet_keys.has_key('data_privkey'):
log.error("No data private key in the wallet. You may need to explicitly select a private key.")
return {'error': 'No data private key set.\nTry passing your owner private key.'}
privkey = wallet_keys['data_privkey']
privkey = ECPrivateKey(privkey).to_hex()
pubkey = get_pubkey_hex(privkey)
res = storage.serialize_mutable_data(data_json, privkey, pubkey, profile=True)
if res is None:
return {'error': 'Failed to sign and serialize profile'}
# sanity check
assert storage.parse_mutable_data(res, pubkey)
return res
def cli_verify_profile( args, config_path=CONFIG_PATH, proxy=None, interactive=False ):
"""
command: verify_profile advanced
help: Verify a profile JWT and deserialize it into a profile object.
arg: name (str) 'The name that points to the public key to use to verify.'
arg: path (str) 'The path to the profile data on disk'
opt: pubkey (str) 'The public key to use to verify. Overrides `name`.'
"""
if proxy is None:
proxy = get_default_proxy(config_path=config_path)
name = str(args.name)
path = str(args.path)
pubkey = None
owner_address = None
if not os.path.exists(path):
return {'error': 'No such file or directory'}
if hasattr(args, 'pubkey') and args.pubkey is not None:
pubkey = str(args.pubkey)
try:
pubkey = ECPublicKey(pubkey).to_hex()
except:
return {'error': 'Invalid public key'}
if pubkey is None:
zonefile_data = None
name_rec = None
# get the pubkey
zonefile_data_res = get_name_zonefile(
name, proxy=proxy, raw_zonefile=True, include_name_record=True
)
if 'error' not in zonefile_data_res:
zonefile_data = zonefile_data_res['zonefile']
name_rec = zonefile_data_res['name_record']
else:
return {'error': "Failed to get zonefile data: {}".format(name)}
# parse
zonefile_dict = None
try:
zonefile_dict = blockstack_zones.parse_zone_file(zonefile_data)
except:
return {'error': 'Nonstandard zone file'}
pubkey = user_zonefile_data_pubkey(zonefile_dict)
if pubkey is None:
# fall back to owner hash
owner_address = str(name_rec['address'])
if virtualchain.is_multisig_address(owner_address):
return {'error': 'No data public key in zone file, and owner is a p2sh address'}
else:
log.warn("Falling back to owner address")
profile_data = None
try:
with open(path, 'r') as f:
profile_data = f.read()
except:
return {'error': 'Failed to read profile file'}
res = storage.parse_mutable_data(profile_data, pubkey, public_key_hash=owner_address)
if res is None:
return {'error': 'Failed to verify profile'}
return res
def cli_sign_data( args, config_path=CONFIG_PATH, proxy=None, password=None, interactive=False ):
"""
command: sign_data advanced raw
help: Sign data to be used in a data store.
arg: path (str) 'The path to the profile data on disk.'
opt: privkey (str) 'The optional private key to sign it with (defaults to the data private key in your wallet)'
"""
if proxy is None:
proxy = get_default_proxy(config_path=config_path)
password = get_default_password(password)
config_dir = os.path.dirname(config_path)
path = str(args.path)
data = None
try:
with open(path, 'r') as f:
data = f.read()
data = data_blob_serialize(data)
except Exception as e:
if os.environ.get("BLOCKSTACK_DEBUG") == "1":
log.exception(e)
log.error("Failed to load {}".format(path))
return {'error': 'Failed to load {}'.format(path)}
privkey = None
if hasattr(args, "privkey") and args.privkey:
privkey = str(args.privkey)
else:
wallet_keys = get_wallet_keys( config_path, password )
if 'error' in wallet_keys:
return wallet_keys
if not wallet_keys.has_key('data_privkey'):
log.error("No data private key in the wallet. You may need to explicitly select a private key.")
return {'error': 'No data private key set.\nTry passing your owner private key.'}
privkey = wallet_keys['data_privkey']
privkey = ECPrivateKey(privkey).to_hex()
pubkey = get_pubkey_hex(privkey)
res = storage.serialize_mutable_data(data, privkey, pubkey)
if res is None:
return {'error': 'Failed to sign and serialize data'}
# sanity check
if BLOCKSTACK_DEBUG:
assert storage.parse_mutable_data(res, pubkey)
if BLOCKSTACK_TEST:
log.debug("Verified {} with {}".format(res, pubkey))
return res
def cli_verify_data( args, config_path=CONFIG_PATH, proxy=None, interactive=True ):
"""
command: verify_data advanced raw
help: Verify signed data and return the payload.
arg: name (str) 'The name that points to the public key to use to verify.'
arg: path (str) 'The path to the profile data on disk'
opt: pubkey (str) 'The public key to use to verify. Overrides `name`.'
"""
if proxy is None:
proxy = get_default_proxy(config_path=config_path)
name = str(args.name)
path = str(args.path)
pubkey = None
if not os.path.exists(path):
return {'error': 'No such file or directory'}
if hasattr(args, 'pubkey') and args.pubkey is not None:
pubkey = str(args.pubkey)
try:
pubkey = keylib.ECPublicKey(pubkey).to_hex()
except Exception as e:
if BLOCKSTACK_DEBUG:
log.exception(e)
return {'error': 'Invalid public key'}
if pubkey is None:
zonefile_data = None
# get the pubkey
zonefile_data_res = get_name_zonefile(
name, proxy=proxy, raw_zonefile=True
)
if 'error' not in zonefile_data_res:
zonefile_data = zonefile_data_res['zonefile']
else:
return {'error': "Failed to get zonefile data: {}".format(name)}
# parse
zonefile_dict = None
try:
zonefile_dict = blockstack_zones.parse_zone_file(zonefile_data)
except:
return {'error': 'Nonstandard zone file'}
pubkey = user_zonefile_data_pubkey(zonefile_dict)
if pubkey is None:
return {'error': 'No data public key in zone file'}
data = None
try:
with open(path, 'r') as f:
data = f.read().strip()
except:
return {'error': 'Failed to read file'}
if BLOCKSTACK_TEST:
log.debug("Verify {} with {}".format(data, pubkey))
res = storage.parse_mutable_data(data, pubkey)
if res is None:
return {'error': 'Failed to verify data'}
return data_blob_parse(res)
def cli_validate_zone_file(args, config_path=CONFIG_PATH, proxy=None):
"""
command: validate_zone_file advanced
help: Validate an on-disk zone file to ensure that is properly formatted.
arg: name (str) 'The name that will use this zone file'
arg: zonefile_path (str) 'The path on disk to the zone file'
opt: verbose (str) 'Pass True to see more analysis beyond "valid" or "invalid".'
"""
name = str(args.name)
zonefile_path = str(args.zonefile_path)
verbose = str(getattr(args, 'verbose', '')).lower() in ['true', 'yes', '1']
if not os.path.exists(zonefile_path):
return {'error': 'No such file or directory: {}'.format(zonefile_path)}
zonefile_data = None
try:
with open(zonefile_path, 'r') as f:
zonefile_data = f.read()
except Exception as e:
if BLOCKSTACK_DEBUG:
log.exception(e)
return {'error': 'Failed to read zone file.'}
res = analyze_zonefile_string(name, zonefile_data, force_data=True, check_current=False, proxy=proxy)
if verbose:
return res
# simplify...
if res['nonstandard']:
return {'error': 'Nonstandard zone file data'}
return {'status': True}
def cli_list_devices( args, config_path=CONFIG_PATH, proxy=None ):
"""
command: list_devices advanced
help: Get the list of device IDs and public keys for a particular application
arg: blockchain_id (str) 'The blockchain ID whose devices to list'
arg: appname (str) 'The name of the application'
"""
raise NotImplemented("Missing token file parsing logic")
def cli_add_device( args, config_path=CONFIG_PATH, proxy=None ):
"""
command: add_device advanced
help: Add a device that can read and write your data
arg: blockchain_id (str) 'The blockchain ID whose profile to update'
opt: device_id (str) 'The ID of the device to add, if not this one'
"""
raise NotImplemented("Missing token file parsing logic")
def cli_remove_device( args, config_path=CONFIG_PATH, proxy=None ):
"""
command: remove_device advanced
help: Remove a device so it can no longer access your application data
arg: blockchain_id (str) 'The blockchain ID whose profile to update'
arg: device_id (str) 'The ID of the device to remove'
"""
raise NotImplemented("Missing token file parsing logic")
def _remove_datastore(rpc, datastore, datastore_privkey, data_pubkeys, rmtree=True, force=False, config_path=CONFIG_PATH ):
"""
Delete a user datastore
If rmtree is True, then the datastore will be emptied first.
If force is True, then the datastore will be deleted even if rmtree fails
Return {'status': True} on success
Return {'error': ...} on error
"""
datastore_pubkey = get_pubkey_hex(datastore_privkey)
datastore_id = datastore_get_id(datastore_pubkey)
# clear the datastore
if rmtree:
log.debug("Clear datastore {}".format(datastore_id))
res = datastore_rmtree(rpc, datastore, '/', datastore_privkey, data_pubkeys, config_path=config_path)
if 'error' in res and not force:
log.error("Failed to rmtree datastore {}".format(datastore_id))
return {'error': 'Failed to remove all files and directories', 'errno': errno.ENOTEMPTY}
# delete the datastore record
log.debug("Delete datastore {}".format(datastore_id))
return delete_datastore(rpc, datastore, datastore_privkey, data_pubkeys, config_path=config_path)
def create_datastore_by_type( datastore_type, blockchain_id, datastore_privkey, session, drivers=None, config_path=CONFIG_PATH ):
"""
Create a datastore or a collection for the given user with the given name.
Return {'status': True} on success
Return {'error': ...} on error
"""
data_pubkeys = jsontokens.decode_token(session)['payload']['app_public_keys']
device_ids = [dk['device_id'] for dk in data_pubkeys]
datastore_pubkey = get_pubkey_hex(datastore_privkey)
datastore_id = datastore_get_id(datastore_pubkey)
rpc = local_api_connect(config_path=config_path, api_session=session)
if rpc is None:
return {'error': 'API endpoint not running. Please start it with `api start`'}
res = rpc.backend_datastore_get(blockchain_id, datastore_id, device_ids)
if 'error' not in res:
# already exists
log.error("Datastore exists")
return {'error': 'Datastore exists', 'errno': errno.EEXIST}
datastore_info = make_datastore_info( datastore_type, datastore_pubkey, device_ids, driver_names=drivers, config_path=config_path)
if 'error' in datastore_info:
return datastore_info
# can put
res = put_datastore(rpc, datastore_info, datastore_privkey, config_path=config_path)
if 'error' in res:
return res
return {'status': True}
def get_datastore_by_type( datastore_type, blockchain_id, datastore_id, device_ids, config_path=CONFIG_PATH ):
"""
Get a datastore or collection.
Return the datastore object on success
Return {'error': ...} on error
"""
rpc = local_api_connect(config_path=config_path)
if rpc is None:
return {'error': 'API endpoint not running. Please start it with `api start`'}
datastore_info = rpc.backend_datastore_get(blockchain_id, datastore_id, device_ids)
if 'error' in datastore_info:
return datastore_info
datastore = datastore_info['datastore']
if datastore['type'] != datastore_type:
return {'error': '{} is a {}'.format(datastore_id, datastore['type'])}
return datastore
def delete_datastore_by_type( datastore_type, blockchain_id, datastore_privkey, session, force=False, config_path=CONFIG_PATH ):
"""
Delete a datastore or collection.
Return {'status': True} on success
Return {'error': ...} on error
"""
datastore_id = datastore_get_id(get_pubkey_hex(datastore_privkey))
data_pubkeys = jsontokens.decode_token(session)['payload']['app_public_keys']
device_ids = [dk['device_id'] for dk in data_pubkeys]
rpc = local_api_connect(config_path=config_path, api_session=session)
if rpc is None:
return {'error': 'API endpoint not running. Please start it with `api start`'}
datastore_info = rpc.backend_datastore_get(blockchain_id, datastore_id, device_ids)
if 'error' in datastore_info:
return datastore_info
datastore = datastore_info['datastore']
if datastore['type'] != datastore_type:
return {'error': '{} is a {}'.format(datastore_id, datastore['type'])}
res = _remove_datastore(rpc, datastore, datastore_privkey, data_pubkeys, rmtree=True, force=force, config_path=config_path)
if 'error' in res:
log.error("Failed to delete datastore record")
return res
return {'status': True}
def datastore_file_get(datastore_type, blockchain_id, datastore_id, path, data_pubkeys, extended=False, force=False, config_path=CONFIG_PATH ):
"""
Get a file from a datastore or collection.
Return {'status': True, 'data': ...} on success
Return {'error': ...} on error
"""
# connect
rpc = local_api_connect(config_path=config_path)
if rpc is None:
return {'error': 'API endpoint not running. Please start it with `api start`'}
device_ids = [dk['device_id'] for dk in data_pubkeys]
datastore_info = rpc.backend_datastore_get(blockchain_id, datastore_id, device_ids)
if 'error' in datastore_info:
if 'errno' not in datastore_info:
datastore_info['errno'] = errno.EPERM
return datastore_info
datastore = datastore_info['datastore']
if datastore['type'] != datastore_type:
return {'error': '{} is a {}'.format(datastore_id, datastore['type'])}
res = datastore_getfile( rpc, blockchain_id, datastore, path, data_pubkeys, extended=extended, force=force, config_path=config_path )
return res
def datastore_file_put(datastore_type, blockchain_id, datastore_privkey, path, data, session, create=False, force_data=False, force=False, config_path=CONFIG_PATH ):
"""
Put a file int oa datastore or collection.
Return {'status': True} on success
Return {'error': ...} on failure.
If this is a collection, then path must be in the root directory
"""
datastore_id = datastore_get_id(get_pubkey_hex(datastore_privkey))
data_pubkeys = jsontokens.decode_token(session)['payload']['app_public_keys']
# is this a path, and are we allowed to take paths?
if is_valid_path(data) and os.path.exists(data) and not force_data:
log.warning("Using data in file {}".format(data))
try:
with open(data) as f:
data = f.read()
except:
return {'error': 'Failed to read "{}"'.format(data)}
# connect
rpc = local_api_connect(config_path=config_path, api_session=session)
if rpc is None:
return {'error': 'API endpoint not running. Please start it with `api start`'}
device_ids = [dk['device_id'] for dk in data_pubkeys]
datastore_info = rpc.backend_datastore_get(blockchain_id, datastore_id, device_ids)
if 'error' in datastore_info:
return datastore_info
datastore = datastore_info['datastore']
log.debug("putfile {} to {}".format(path, datastore_id))
res = datastore_putfile( rpc, datastore, path, data, datastore_privkey, data_pubkeys, create=create, config_path=config_path )
if 'error' in res:
return res
return res
def datastore_dir_list(datastore_type, blockchain_id, datastore_id, path, data_pubkeys, extended=False, force=False, config_path=CONFIG_PATH ):
"""
List a directory in a datastore or collection
Return {'status': True, 'dir': ...} on success
Return {'error': ...} on error
"""
# connect
rpc = local_api_connect(config_path=config_path)
if rpc is None:
return {'error': 'API endpoint not running. Please start it with `api start`'}
device_ids = [dk['device_id'] for dk in data_pubkeys]
datastore_info = rpc.backend_datastore_get(blockchain_id, datastore_id, device_ids)
if 'error' in datastore_info:
if 'errno' not in datastore_info:
datastore_info['errno'] = errno.EPERM
return datastore_info
datastore = datastore_info['datastore']
if datastore['type'] != datastore_type:
return {'error': '{} is a {}'.format(datastore_id, datastore['type']), 'errno': errno.EINVAL}
if datastore_type == 'collection':
# can only be '/'
if path != '/':
return {'error': 'Invalid argument: collections do not have directories', 'errno': errno.EINVAL}
res = datastore_listdir( rpc, blockchain_id, datastore, path, data_pubkeys, extended=extended, force=force, config_path=config_path )
return res
def datastore_path_stat(datastore_type, blockchain_id, datastore_id, path, data_pubkeys, extended=False, force=False, config_path=CONFIG_PATH ):
"""
Stat a path in a datastore or collection
Return {'status': True, 'inode': ...} on success
Return {'error': ...} on error
"""
# connect
rpc = local_api_connect(config_path=config_path)
if rpc is None:
return {'error': 'API endpoint not running. Please start it with `api start`'}
device_ids = [dk['device_id'] for dk in data_pubkeys]
datastore_info = rpc.backend_datastore_get(blockchain_id, datastore_id, device_ids)
if 'error' in datastore_info:
return datastore_info
datastore = datastore_info['datastore']
if datastore['type'] != datastore_type:
return {'error': '{} is a {}'.format(datastore_id, datastore['type'])}
res = datastore_stat( rpc, blockchain_id, datastore, path, data_pubkeys, extended=extended, force=force, config_path=config_path )
return res
def datastore_inode_getinode(datastore_type, blockchain_id, datastore_id, inode_uuid, data_pubkeys, extended=False, force=False, idata=False, config_path=CONFIG_PATH ):
"""
Get an inode in a datastore or collection
Return {'status': True, 'inode': ...} on success
Return {'error': ...} on error
"""
# connect
rpc = local_api_connect(config_path=config_path)
if rpc is None:
return {'error': 'API endpoint not running. Please start it with `api start`'}
device_ids = [dk['device_id'] for dk in data_pubkeys]
datastore_info = rpc.backend_datastore_get(blockchain_id, datastore_id, device_ids)
if 'error' in datastore_info:
return datastore_info
datastore = datastore_info['datastore']
if datastore['type'] != datastore_type:
return {'error': '{} is a {}'.format(datastore_id, datastore['type'])}
res = datastore_getinode( rpc, blockchain_id, datastore, inode_uuid, data_pubkeys, extended=False, force=force, idata=idata, config_path=config_path )
return res
def cli_get_device_keys( args, config_path=CONFIG_PATH ):
"""
command: get_device_keys advanced
help: Get the device IDs and public keys for a blockchain ID
arg: blockchain_id (str) 'The blockchain Id'
"""
blockchain_id = str(args.blockchain_id)
# TODO: implement token file support
log.warning("Token file support is NOT IMPLEMENTED")
# find the "data public key"
def cli_get_datastore( args, config_path=CONFIG_PATH ):
"""
command: get_datastore advanced
help: Get a datastore record
arg: blockchain_id (str) 'The ID of the owner'
arg: datastore_id (str) 'The application datastore ID'
opt: device_ids (str) 'The CSV of device IDs owned by the blockchain ID'
"""
blockchain_id = str(args.blockchain_id)
datastore_id = str(args.datastore_id)
# get the list of device IDs to use
device_ids = getattr(args, 'device_ids', None)
if device_ids:
device_ids = device_ids.split(',')
else:
raise NotImplemented("Missing token file parsing logic")
return get_datastore_by_type('datastore', blockchain_id, datastore_id, device_ids, config_path=config_path )
def cli_create_datastore( args, config_path=CONFIG_PATH, proxy=None ):
"""
command: create_datastore advanced
help: Make a new datastore
arg: blockchain_id (str) 'The blockchain ID that will own this datastore'
arg: privkey (str) 'The ECDSA private key of the datastore'
arg: session (str) 'The API session token'
opt: drivers (str) 'A CSV of drivers to use.'
"""
if proxy is None:
proxy = get_default_proxy()
blockchain_id = str(args.blockchain_id)
privkey = str(args.privkey)
drivers = getattr(args, 'drivers', None)
if drivers:
drivers = drivers.split(',')
return create_datastore_by_type('datastore', blockchain_id, privkey, str(args.session), drivers=drivers, config_path=config_path )
def cli_delete_datastore( args, config_path=CONFIG_PATH ):
"""
command: delete_datastore advanced
help: Delete a datastore owned by a given user, and all of the data it contains.
arg: blockchain_id (str) 'The owner of this datastore'
arg: privkey (str) 'The ECDSA private key of the datastore'
arg: session (str) 'The API session token'
opt: force (str) 'If True, then delete the datastore even if it cannot be emptied'
"""
blockchain_id = str(args.blockchain_id)
privkey = str(args.privkey)
force = False
if hasattr(args, 'force'):
force = (str(args.force).lower() in ['1', 'true', 'force', 'yes'])
return delete_datastore_by_type('datastore', blockchain_id, privkey, str(args.session), force=force, config_path=config_path)
def cli_datastore_mkdir( args, config_path=CONFIG_PATH, interactive=False ):
"""
command: datastore_mkdir advanced
help: Make a directory in a datastore.
arg: blockchain_id (str) 'The owner of this datastore'
arg: privkey (str) 'The app-specific private key'
arg: path (str) 'The path to the directory to remove'
arg: session (str) 'The API session token'
"""
blockchain_id = str(args.blockchain_id)
path = str(args.path)
datastore_privkey_hex = str(args.privkey)
datastore_pubkey_hex = get_pubkey_hex(datastore_privkey_hex)
datastore_id = datastore_get_id(datastore_pubkey_hex)
session = jsontokens.decode_token(str(args.session))
data_pubkeys = session['payload']['app_public_keys']
device_ids = [dk['device_id'] for dk in data_pubkeys]
# connect
rpc = local_api_connect(config_path=config_path, api_session=str(args.session))
if rpc is None:
return {'error': 'API endpoint not running. Please start it with `api start`'}
datastore_info = rpc.backend_datastore_get(blockchain_id, datastore_id, device_ids)
if 'error' in datastore_info:
return datastore_info
datastore = datastore_info['datastore']
assert datastore_id == datastore_get_id(get_pubkey_hex(datastore_privkey_hex))
res = datastore_mkdir(rpc, datastore, path, datastore_privkey_hex, data_pubkeys, config_path=config_path )
if 'error' in res:
return res
# make url
if not path.endswith('/'):
path += '/'
return res
def cli_datastore_rmdir( args, config_path=CONFIG_PATH, interactive=False ):
"""
command: datastore_rmdir advanced
help: Remove a directory in a datastore.
arg: blockchain_id (str) 'The owner of this datastore'
arg: privkey (str) 'The app-specific data private key'
arg: path (str) 'The path to the directory to remove'
arg: session (str) 'The API session token'
"""
blockchain_id = str(args.blockchain_id)
path = str(args.path)
datastore_privkey_hex = str(args.privkey)
datastore_pubkey_hex = get_pubkey_hex(datastore_privkey_hex)
datastore_id = datastore_get_id(datastore_pubkey_hex)
force = (str(getattr(args, 'force', '').lower()) in ['1', 'true'])
session = jsontokens.decode_token(str(args.session))
data_pubkeys = session['payload']['app_public_keys']
device_ids = [dk['device_id'] for dk in data_pubkeys]
# connect
rpc = local_api_connect(config_path=config_path, api_session=str(args.session))
if rpc is None:
return {'error': 'API endpoint not running. Please start it with `api start`'}
datastore_info = rpc.backend_datastore_get(blockchain_id, datastore_id, device_ids)
if 'error' in datastore_info:
return datastore_info
datastore = datastore_info['datastore']
assert datastore_id == datastore_get_id(get_pubkey_hex(datastore_privkey_hex))
res = datastore_rmdir(rpc, datastore, path, datastore_privkey_hex, data_pubkeys, force=force, config_path=config_path )
return res
def cli_datastore_rmtree( args, config_path=CONFIG_PATH, interactive=False ):
"""
command: datastore_rmtree advanced
help: Remove a directory and all its children from a datastore.
arg: blockchain_id (str) 'The owner of this datastore'
arg: privkey (str) 'The app-specific data private key'
arg: path (str) 'The path to the directory tree to remove'
arg: session (str) 'The API session token'
"""
blockchain_id = str(args.blockchain_id)
path = str(args.path)
datastore_privkey_hex = str(args.privkey)
datastore_pubkey_hex = get_pubkey_hex(datastore_privkey_hex)
datastore_id = datastore_get_id(datastore_pubkey_hex)
session = jsontokens.decode_token(str(args.session))
data_pubkeys = session['payload']['app_public_keys']
device_ids = [dk['device_id'] for dk in data_pubkeys]
# connect
rpc = local_api_connect(config_path=config_path, api_session=str(args.session))
if rpc is None:
return {'error': 'API endpoint not running. Please start it with `api start`'}
datastore_info = rpc.backend_datastore_get(blockchain_id, datastore_id, device_ids)
if 'error' in datastore_info:
return datastore_info
datastore = datastore_info['datastore']
assert datastore_id == datastore_get_id(get_pubkey_hex(datastore_privkey_hex))
res = datastore_rmtree(rpc, datastore, path, datastore_privkey_hex, data_pubkeys, config_path=config_path )
return res
def cli_datastore_getfile( args, config_path=CONFIG_PATH, interactive=False ):
"""
command: datastore_getfile advanced raw
help: Get a file from a datastore.
arg: blockchain_id (str) 'The ID of the datastore owner'
arg: datastore_id (str) 'The ID of the application datastore'
arg: path (str) 'The path to the file to load'
opt: extended (str) 'If True, then include the full inode and parent information as well.'
opt: force (str) 'If True, then tolerate stale data faults.'
opt: device_ids (str) 'If given, a CSV of device IDs owned by the blockchain ID'
opt: device_pubkeys (str) 'If given, a CSV of device public keys owned by the blockchain ID'
"""
blockchain_id = getattr(args, 'blockchain_id', '')
if len(blockchain_id) == 0:
blockchain_id = None
else:
blockchain_id = str(blockchain_id)
datastore_id = str(args.datastore_id)
path = str(args.path)
extended = False
force = False
device_ids = None
if hasattr(args, 'extended') and args.extended.lower() in ['1', 'true']:
extended = True
if hasattr(args, 'force') and args.force.lower() in ['1', 'true']:
force = True
# get the list of device IDs to use
device_ids = getattr(args, 'device_ids', None)
if device_ids:
device_ids = device_ids.split(',')
else:
raise NotImplemented("Missing token file parsing logic")
device_pubkeys = None
if hasattr(args, 'device_pubkeys'):
device_pubkeys = str(args.device_pubkeys).split(',')
else:
raise NotImplemented("No support for token files")
assert len(device_ids) == len(device_pubkeys)
data_pubkeys = [{
'device_id': dev_id,
'public_key': pubkey
} for (dev_id, pubkey) in zip(device_ids, device_pubkeys)]
res = datastore_file_get('datastore', blockchain_id, datastore_id, path, data_pubkeys, extended=extended, force=force, config_path=config_path)
if json_is_error(res):
return res
if not extended:
# just the data
return res['data']
return res
def cli_datastore_listdir(args, config_path=CONFIG_PATH, interactive=False ):
"""
command: datastore_listdir advanced
help: List a directory in the datastore.
arg: blockchain_id (str) 'The ID of the datastore owner'
arg: datastore_id (str) 'The ID of the application datastore'
arg: path (str) 'The path to the directory to list'
opt: extended (str) 'If True, then include the full inode and parent information as well.'
opt: force (str) 'If True, then tolerate stale data faults.'
opt: device_ids (str) 'If given, a CSV of device IDs owned by the blockchain ID'
opt: device_pubkeys (str) 'If given, a CSV of device public keys owned by the blockchain ID'
"""
blockchain_id = getattr(args, 'blockchain_id', '')
if len(blockchain_id) == 0:
blockchain_id = None
else:
blockchain_id = str(blockchain_id)
datastore_id = str(args.datastore_id)
path = str(args.path)
extended = False
force = False
device_ids = None
if hasattr(args, 'extended') and args.extended.lower() in ['1', 'true']:
extended = True
if hasattr(args, 'force') and args.force.lower() in ['1', 'true']:
force = True
# get the list of device IDs to use
device_ids = getattr(args, 'device_ids', None)
if device_ids:
device_ids = device_ids.split(',')
else:
raise NotImplemented("Missing token file parsing logic")
device_pubkeys = None
if hasattr(args, 'device_pubkeys'):
device_pubkeys = str(args.device_pubkeys).split(',')
else:
raise NotImplemented("No support for token files")
assert len(device_ids) == len(device_pubkeys)
data_pubkeys = [{
'device_id': dev_id,
'public_key': pubkey
} for (dev_id, pubkey) in zip(device_ids, device_pubkeys)]
res = datastore_dir_list('datastore', blockchain_id, datastore_id, path, data_pubkeys, extended=extended, force=force, config_path=config_path )
if json_is_error(res):
return res
if not extended:
return res['data']
return res
def cli_datastore_stat(args, config_path=CONFIG_PATH, interactive=False ):
"""
command: datastore_stat advanced
help: Stat a file or directory in the datastore, returning only the header for files but returning the entire listing for directories.
arg: blockchain_id (str) 'The ID of the datastore owner'
arg: datastore_id (str) 'The datastore ID'
arg: path (str) 'The path to the file or directory to stat'
opt: extended (str) 'If True, then include the path information as well'
opt: force (str) 'If True, then tolerate stale inode data.'
opt: device_ids (str) 'If given, a CSV of device IDs owned by the blockchain ID'
opt: device_pubkeys (str) 'If given, a CSV of device public keys owned by the blockchain ID'
"""
blockchain_id = getattr(args, 'blockchain_id', '')
if len(blockchain_id) == 0:
blockchain_id = None
else:
blockchain_id = str(blockchain_id)
path = str(args.path)
datastore_id = str(args.datastore_id)
path = str(args.path)
extended = False
force = False
device_ids = None
if hasattr(args, 'extended') and args.extended.lower() in ['1', 'true']:
extended = True
if hasattr(args, 'force') and args.force.lower() in ['1', 'true']:
force = True
# get the list of device IDs to use
device_ids = getattr(args, 'device_ids', None)
if device_ids:
device_ids = device_ids.split(',')
else:
# TODO
raise NotImplemented("Missing token file parsing logic")
device_pubkeys = None
if hasattr(args, 'device_pubkeys'):
device_pubkeys = str(args.device_pubkeys).split(',')
else:
# TODO
raise NotImplemented("No support for token files")
assert len(device_ids) == len(device_pubkeys)
data_pubkeys = [{
'device_id': dev_id,
'public_key': pubkey
} for (dev_id, pubkey) in zip(device_ids, device_pubkeys)]
res = datastore_path_stat('datastore', blockchain_id, datastore_id, path, data_pubkeys, extended=extended, force=force, config_path=config_path)
if json_is_error(res):
return res
if not extended:
return res['data']
return res
def cli_datastore_getinode(args, config_path=CONFIG_PATH, interactive=False):
"""
command: datastore_getinode advanced
help: Get a raw inode from a datastore
arg: blockchain_id (str) 'The ID of the datastore owner'
arg: datastore_id (str) 'The ID of the application user'
arg: inode_uuid (str) 'The inode UUID'
opt: extended (str) 'If True, then include the path information as well'
opt: idata (str) 'If True, then include the inode payload as well.'
opt: force (str) 'If True, then tolerate stale inode data.'
opt: device_ids (str) 'If given, the CSV of devices owned by the blockchain ID'
opt: device_pubkeys (str) 'If given, the CSV of device public keys owned by the blockchain ID'
"""
blockchain_id = getattr(args, 'blockchain_id', '')
if len(blockchain_id) == 0:
blockchain_id = None
else:
blockchain_id = str(blockchain_id)
datastore_id = str(args.datastore_id)
inode_uuid = str(args.inode_uuid)
session = jsontokens.decode_token(str(args.session))
data_pubkeys = session['payload']['app_public_keys']
extended = False
force = False
idata = False
device_ids = None
if hasattr(args, 'extended') and args.extended.lower() in ['1', 'true']:
extended = True
if hasattr(args, 'force') and args.force.lower() in ['1', 'true']:
force = True
if hasattr(args, 'idata') and args.idata.lower() in ['1', 'true']:
idata = True
# get the list of device IDs to use
device_ids = getattr(args, 'device_ids', None)
if device_ids:
device_ids = device_ids.split(',')
else:
# TODO
raise NotImplemented("Missing token file parsing logic")
device_pubkeys = None
if hasattr(args, 'device_pubkeys'):
device_pubkeys = str(args.device_pubkeys).split(',')
else:
# TODO
raise NotImplemented("No support for token files")
assert len(device_ids) == len(device_pubkeys)
data_pubkeys = [{
'device_id': dev_id,
'public_key': pubkey
} for (dev_id, pubkey) in zip(device_ids, device_pubkeys)]
return datastore_inode_getinode('datastore', blockchain_id, datastore_id, inode_uuid, data_pubkeys, extended=extended, force=force, idata=idata, config_path=config_path)
def cli_datastore_putfile(args, config_path=CONFIG_PATH, interactive=False, force_data=False ):
"""
command: datastore_putfile advanced
help: Put a file into the datastore at the given path.
arg: blockchain_id (str) 'The owner of this datastore'
arg: privkey (str) 'The app-specific data private key'
arg: path (str) 'The path to the new file'
arg: data (str) 'The data to store, or a path to a file with the data'
arg: session (str) 'The API session token'
opt: create (str) 'If True, then succeed only if the file has never before existed.'
opt: force (str) 'If True, then tolerate stale inode data.'
"""
blockchain_id = str(args.blockchain_id)
path = str(args.path)
data = str(args.data)
privkey = str(args.privkey)
create = (str(getattr(args, "create", "")).lower() in ['1', 'create', 'true'])
force = (str(getattr(args, 'force', '')).lower() in ['1', 'true'])
return datastore_file_put('datastore', blockchain_id, privkey, path, data, str(args.session), create=create, force_data=force_data, config_path=config_path )
def cli_datastore_deletefile(args, config_path=CONFIG_PATH, interactive=False ):
"""
command: datastore_deletefile advanced
help: Delete a file from the datastore.
arg: blockchain_id (str) 'The owner of this datastore'
arg: privkey (str) 'The datastore private key'
arg: path (str) 'The path to the file to delete'
arg: session (str) 'The API session token'
opt: force (str) 'If True, then tolerate stale inode data.'
"""
blockchain_id = str(args.blockchain_id)
path = str(args.path)
privkey = str(args.privkey)
datastore_id = datastore_get_id(get_pubkey_hex(privkey))
force = (str(getattr(args, 'force', '')).lower() in ['1', 'true'])
session = jsontokens.decode_token(str(args.session))
data_pubkeys = session['payload']['app_public_keys']
device_ids = [dk['device_id'] for dk in session['payload']['app_public_keys']]
# connect
rpc = local_api_connect(config_path=config_path)
if rpc is None:
return {'error': 'API endpoint not running. Please start it with `api start`'}
datastore_info = rpc.backend_datastore_get(blockchain_id, datastore_id, device_ids)
if 'error' in datastore_info:
datastore_info['errno'] = errno.EPERM
return datastore_info
datastore = datastore_info['datastore']
res = datastore_deletefile( rpc, datastore, path, privkey, data_pubkeys, force=force, config_path=config_path )
return res
def cli_datastore_get_privkey(args, config_path=CONFIG_PATH, interactive=False ):
"""
command: datastore_get_privkey advanced
help: Get the private key for a datastore, given the master private key.
arg: master_privkey (str) 'The master data private key'
arg: app_domain (str) 'The name of the application'
"""
raise NotImplemented("Token file support not yet implemented")
app_domain = str(args.app_domain)
master_privkey = str(args.master_privkey)
datastore_privkey = datastore_get_privkey(master_privkey, app_domain, config_path=config_path)
return {'status': True, 'datastore_privkey': datastore_privkey}
def cli_datastore_get_id(args, config_path=CONFIG_PATH, interactive=False ):
"""
command: datastore_get_id advanced
help: Get the ID of an application data store
arg: datastore_privkey (str) 'The datastore private key'
"""
datastore_id = datastore_get_id(get_pubkey_hex(str(args.datastore_privkey)))
return {'status': True, 'datastore_id': datastore_id}
def cli_get_collection( args, config_path=CONFIG_PATH, proxy=None, password=None ):
"""
command: get_collection advanced
help: Get a collection record
arg: blockchain_id (str) 'The ID of the owner'
arg: collection_domain (str) 'The name of the collection'
opt: device_ids (str) 'The list of device IDs that can write'
"""
raise NotImplemented("Collections not implemented")
blockchain_id = str(args.blockchain_id)
collection_domain = str(args.collection_domain)
device_ids = args.device_ids.split(',')
return get_datastore_by_type('collection', blockchain_id, collection_domain, device_ids, config_path=config_path )
def cli_create_collection( args, config_path=CONFIG_PATH, proxy=None, password=None, master_data_privkey=None ):
"""
command: create_collection advanced
help: Make a new collection for a given user.
arg: blockchain_id (str) 'The blockchain ID that will own this collection'
arg: collection_domain (str) 'The domain of this collection.'
opt: device_ids (str) 'A CSV of your device IDs.'
"""
if proxy is None:
proxy = get_default_proxy(config_path)
raise NotImplemented("Collections not implemented")
blockchain_id = str(args.blockchain_id)
password = get_default_password(password)
collection_domain = str(args.collection_domain)
# get the list of device IDs to use
device_ids = getattr(args, 'device_ids', None)
if device_ids:
device_ids = device_ids.split(',')
else:
raise NotImplemented("Missing token file parsing logic")
# TODO: privkey
# privkey =
raise NotImplemented("Collections are not implemented yet")
# get the list of device IDs to use
device_ids = getattr(args, 'device_ids', None)
if device_ids:
device_ids = device_ids.split(',')
else:
raise NotImplemented("Missing token file parsing logic")
return create_datastore_by_type('collection', blockchain_id, privkey, device_ids, proxy=proxy, config_path=config_path, password=password, master_data_privkey=master_data_privkey)
def cli_delete_collection( args, config_path=CONFIG_PATH, proxy=None, password=None, master_data_privkey=None ):
"""
command: delete_collection advanced
help: Delete a collection owned by a given user, and all of the data it contains.
arg: blockchain_id (str) 'The owner of this collection'
arg: collection_domain (str) 'The domain of this collection'
opt: device_ids (str) 'A CSV of your devices'
"""
if proxy is None:
proxy = get_default_proxy(config_path)
raise NotImplemented("Collections not implemented")
password = get_default_password(password)
blockchain_id = str(args.blockchain_id)
collection_domain = str(args.collection_domain)
# get the list of device IDs to use
device_ids = getattr(args, 'device_ids', None)
if device_ids:
device_ids = device_ids.split(',')
else:
raise NotImplemented("Missing token file parsing logic")
# TODO: privkey
# privkey =
raise NotImplemented("Collections are not implemented")
device_ids = None
if hasattr(args, 'device_ids'):
device_ids = str(args.device_ids).split(',')
else:
raise NotImplemented("No support for token files")
return delete_datastore_by_type('collection', blockchain_id, privkey, device_ids, force=True, config_path=config_path, proxy=proxy, password=password)
def cli_collection_listitems(args, config_path=CONFIG_PATH, password=None, interactive=False, proxy=None ):
"""
command: collection_items advanced
help: List the contents of a collection
arg: blockchain_id (str) 'The ID of the collection owner'
arg: collection_domain (str) 'The domain of this collection'
arg: path (str) 'The path to the directory to list'
"""
if proxy is None:
proxy = get_default_proxy(config_path)
raise NotImplemented("Collections not implemented")
password = get_default_password(password)
blockchain_id = str(args.blockchain_id)
# get the list of device IDs to use
device_ids = getattr(args, 'device_ids', None)
if device_ids:
device_ids = device_ids.split(',')
else:
raise NotImplemented("Missing token file parsing logic")
collection_domain = str(args.collection_domain)
path = str(args.path)
res = datastore_dir_list('collection', blockchain_id, collection_domain, '/', device_ids, config_path=config_path, proxy=proxy)
if 'error' in res:
return res
# if somehow we get a directory in here, exclude it
dir_info = res['dir']
filtered_dir_info = {}
for name in dir_info.keys():
if dir_info[name]['type'] == MUTABLE_DATUM_FILE_TYPE:
filtered_dir_info[name] = dir_info[name]
return {'status': True, 'dir': filtered_dir_info}
def cli_collection_statitem(args, config_path=CONFIG_PATH, interactive=False, proxy=None):
"""
command: collection_statitem advanced
help: Stat an item in a collection
arg: blockchain_id (str) 'The ID of the collection owner'
arg: collection_domain (str) 'The name of this collection'
arg: item_id (str) 'The name of the item to stat'
"""
if proxy is None:
proxy = get_default_proxy(config_path)
raise NotImplemented("Collections not implemented")
password = get_default_password(password)
# get the list of device IDs to use
device_ids = getattr(args, 'device_ids', None)
if device_ids:
device_ids = device_ids.split(',')
else:
raise NotImplemented("Missing token file parsing logic")
blockchain_id = str(args.blockchain_id)
collection_domain = str(args.collection_domain)
item_id = str(args.item_id)
return datastore_path_stat('collection', blockchain_id, collection_domain, '/{}'.format(item_id), device_ids, proxy=proxy, config_path=config_path)
def cli_collection_putitem(args, config_path=CONFIG_PATH, interactive=False, proxy=None, password=None, force_data=False, master_data_privkey=None ):
"""
command: collection_putitem advanced
help: Put an item into a collection. Overwrites are forbidden.
arg: blockchain_id (str) 'The owner of the collection'
arg: collection_privkey (str) 'The collection private key'
arg: item_id (str) 'The item name'
arg: data (str) 'The data to store, or a path to a file with the data'
opt: device_ids (str) 'A CSV of your device IDs'
"""
if proxy is None:
proxy = get_default_proxy(config_path)
raise NotImplemented("Collections not implemented")
password = get_default_password(password)
blockchain_id = str(args.blockchain_id)
collection_domain = str(args.collection_domain)
item_id = str(args.item_id)
data = args.data
collection_privkey = str(args.collection_privkey)
# get the list of device IDs to use
device_ids = getattr(args, 'device_ids', None)
if device_ids:
device_ids = device_ids.split(',')
else:
raise NotImplemented("Missing token file parsing logic")
return datastore_file_put('collection', blockchain_id, collection_privkey, '/{}'.format(item_id), data, device_ids=device_ids,
create=True, force_data=force_data, proxy=proxy, config_path=config_path, master_data_privkey=master_data_privkey, password=password)
def cli_collection_getitem( args, config_path=CONFIG_PATH, interactive=False, password=None, proxy=None ):
"""
command: collection_getitem advanced
help: Get an item from a collection.
arg: blockchain_id (str) 'The ID of the datastore owner'
arg: collection_domain (str) 'The domain of this collection'
arg: item_id (str) 'The item to fetch'
"""
if proxy is None:
proxy = get_default_proxy(config_path)
raise NotImplemented("Collections not implemented")
password = get_default_password(password)
config_dir = os.path.dirname(config_path)
blockchain_id = getattr(args, 'blockchain_id', '')
if len(blockchain_id) == 0:
blockchain_id = None
else:
blockchain_id = str(blockchain_id)
collection_domain = str(args.collection_domain)
item_id = str(args.item_id)
return datastore_file_get('collection', blockchain_id, collection_domain, '/{}'.format(item_id), password=password, config_path=config_path, proxy=proxy)
def cli_start_server( args, config_path=CONFIG_PATH, interactive=False ):
"""
command: start_server advanced
help: Start a Blockstack indexing server
opt: foreground (str) 'If True, then run in the foreground.'
opt: working_dir (str) 'The directory which contains the server state.'
opt: testnet (str) 'If True, then communicate with Bitcoin testnet.'
"""
foreground = False
testnet = False
working_dir = args.working_dir
if args.foreground:
foreground = str(args.foreground)
foreground = (foreground.lower() in ['1', 'true', 'yes', 'foreground'])
if args.testnet:
testnet = str(args.testnet)
testnet = (testnet.lower() in ['1', 'true', 'yes', 'testnet3'])
cmds = ['blockstack-core', 'start']
if foreground:
cmds.append('--foreground')
if testnet:
cmds.append('--testnet3')
# TODO: use subprocess
if working_dir is not None:
working_dir_envar = 'VIRTUALCHAIN_WORKING_DIR="{}"'.format(working_dir)
cmds = [working_dir_envar] + cmds
cmd_str = " ".join(cmds)
log.debug('Execute: {}'.format(cmd_str))
exit_status = os.system(cmd_str)
if not os.WIFEXITED(exit_status) or os.WEXITSTATUS(exit_status) != 0:
error_str = 'Failed to execute "{}". Exit code {}'.format(cmd_str, exit_status)
return {'error': error_str}
return {'status': True}
def cli_stop_server( args, config_path=CONFIG_PATH, interactive=False ):
"""
command: stop_server advanced
help: Stop a running Blockstack indexing server
opt: working_dir (str) 'The directory which contains the server state.'
"""
working_dir = args.working_dir
cmds = ['blockstack-core', 'stop']
if working_dir is not None:
working_dir_envar = 'VIRTUALCHAIN_WORKING_DIR="{}"'.format(working_dir)
cmds = [working_dir_envar] + cmds
cmd_str = " ".join(cmds)
# TODO: use subprocess
log.debug('Execute: {}'.format(cmd_str))
exit_status = os.system(cmd_str)
if not os.WIFEXITED(exit_status) or os.WEXITSTATUS(exit_status) != 0:
error_str = 'Failed to execute "{}". Exit code {}'.format(cmd_str, exit_status)
return {'error': error_str}
return {'status': True}