mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-03-26 22:39:00 +08:00
1975 lines
76 KiB
Python
1975 lines
76 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
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/>.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import simplejson
|
|
import pybitcoin
|
|
import traceback
|
|
import time
|
|
|
|
# Hack around absolute paths
|
|
current_dir = os.path.abspath(os.path.dirname(__file__))
|
|
parent_dir = os.path.abspath(current_dir + "/../")
|
|
|
|
from .queue import in_queue, queue_append, queue_findone
|
|
|
|
from .blockchain import get_tx_confirmations
|
|
from .blockchain import is_address_usable
|
|
from .blockchain import can_receive_name, get_balance, get_tx_fee, get_utxos
|
|
from .blockchain import get_block_height
|
|
|
|
from crypto.utils import get_address_from_privkey, get_pubkey_from_privkey
|
|
|
|
from ..utils import pretty_print as pprint
|
|
from ..utils import pretty_dump
|
|
|
|
from ..config import PREORDER_CONFIRMATIONS, DEFAULT_QUEUE_PATH, CONFIG_PATH, get_utxo_provider_client, get_tx_broadcaster, RPC_MAX_ZONEFILE_LEN, RPC_MAX_PROFILE_LEN
|
|
from ..config import get_logger, APPROX_TX_IN_P2PKH_LEN, APPROX_TX_OUT_P2PKH_LEN, APPROX_TX_OVERHEAD_LEN, APPROX_TX_IN_P2SH_LEN, APPROX_TX_OUT_P2SH_LEN
|
|
from ..constants import BLOCKSTACK_TEST, BLOCKSTACK_DEBUG, TX_MIN_CONFIRMATIONS
|
|
|
|
from ..proxy import get_default_proxy
|
|
from ..proxy import getinfo as blockstack_getinfo
|
|
from ..proxy import get_name_cost as blockstack_get_name_cost
|
|
from ..proxy import get_name_blockchain_record as blockstack_get_name_blockchain_record
|
|
from ..proxy import get_namespace_blockchain_record as blockstack_get_namespace_blockchain_record
|
|
from ..proxy import is_name_registered, is_name_owner
|
|
|
|
from ..tx import sign_tx, sign_and_broadcast_tx, preorder_tx, register_tx, update_tx, transfer_tx, revoke_tx, \
|
|
namespace_preorder_tx, namespace_reveal_tx, namespace_ready_tx, announce_tx, name_import_tx, sign_tx
|
|
|
|
from ..scripts import tx_make_subsidizable
|
|
from ..storage import get_blockchain_compat_hash, hash_zonefile, put_announcement, get_zonefile_data_hash
|
|
|
|
from ..operations import fees_update, fees_transfer, fees_revoke, fees_registration, fees_preorder, \
|
|
fees_namespace_preorder, fees_namespace_reveal, fees_namespace_ready, fees_announce
|
|
|
|
from ..keys import get_privkey_info_address, get_privkey_info_params
|
|
|
|
from .safety import *
|
|
|
|
import virtualchain
|
|
|
|
log = get_logger("blockstack-client")
|
|
|
|
|
|
class UTXOWrapper(object):
|
|
"""
|
|
Class for wrapping a known list of UTXOs for a set of addresses.
|
|
Compatible with pybitcoin's UTXO service class.
|
|
Requires get_unspents()
|
|
"""
|
|
def __init__(self):
|
|
self.utxos = {}
|
|
|
|
def add_unspents( self, addr, unspents ):
|
|
# sanity check...
|
|
for unspent in unspents:
|
|
assert unspent.has_key('transaction_hash')
|
|
assert unspent.has_key('output_index')
|
|
|
|
if not self.utxos.has_key(addr):
|
|
self.utxos[addr] = []
|
|
|
|
self.utxos[addr] += unspents
|
|
|
|
def get_unspents( self, addr ):
|
|
if addr not in self.utxos:
|
|
raise ValueError("No unspents for address {}".format(addr))
|
|
|
|
return self.utxos[addr]
|
|
|
|
|
|
def estimate_dust_fee( tx, fee_estimator ):
|
|
"""
|
|
Estimate the dust fee of an operation.
|
|
fee_estimator is a callable, and is one of the operation's get_fees() methods.
|
|
Return the number of satoshis on success
|
|
Return None on error
|
|
"""
|
|
tx = virtualchain.tx_deserialize( tx )
|
|
tx_inputs = tx['vin']
|
|
tx_outputs = tx['vout']
|
|
dust_fee, op_fee = fee_estimator( tx_inputs, tx_outputs )
|
|
log.debug("dust_fee is %s" % dust_fee)
|
|
return dust_fee
|
|
|
|
|
|
def make_fake_privkey_info( privkey_params ):
|
|
"""
|
|
Make fake private key information, given parameters.
|
|
Used for generating fake transactions and estimating fees.
|
|
@privkey_params is a 2-tuple, with m and n (m of n signatures).
|
|
(1, 1) means "use a single private key"
|
|
(m, n) means "use multiple signatures and a redeem script"
|
|
"""
|
|
if privkey_params is None or privkey_params[0] < 1 or privkey_params[1] < 1:
|
|
raise Exception("Invalid private key parameters %s" % str(privkey_params))
|
|
|
|
if privkey_params == (1, 1):
|
|
# fake private key
|
|
return "5512612ed6ef10ea8c5f9839c63f62107c73db7306b98588a46d0cd2c3d15ea5"
|
|
|
|
else:
|
|
m, n = privkey_params
|
|
return virtualchain.make_multisig_wallet( m, n )
|
|
|
|
|
|
def estimate_payment_bytes( payment_address, utxo_client, num_payment_sigs=None, config_path=CONFIG_PATH ):
|
|
"""
|
|
Given the payment address and number of owner signatures, estimate how many
|
|
extra bytes will be needed to include the payment inputs and outputs
|
|
|
|
Return the number of bytes on success
|
|
Raise ValueError if there are no UTXOs
|
|
Raise Exception if we can't query UTXOs
|
|
"""
|
|
|
|
payment_utxos = get_utxos( payment_address, config_path=config_path, utxo_client=utxo_client )
|
|
if payment_utxos is None:
|
|
log.error("No UTXOs returned")
|
|
raise ValueError()
|
|
|
|
if 'error' in payment_utxos:
|
|
log.error("Failed to query UTXOs for %s: %s" % payment_address, payment_utxos['error'])
|
|
raise Exception("Failed to query UTXO provider: %s" % payment_utxos['error'])
|
|
|
|
num_payment_inputs = len(payment_utxos)
|
|
if num_payment_inputs == 0:
|
|
# assume at least one payment UTXO
|
|
num_payment_inputs = 1
|
|
|
|
if num_payment_sigs is None:
|
|
# try to guess from the address
|
|
if virtualchain.is_p2sh_address(payment_address):
|
|
log.warning("Assuming 2 signatures required from p2sh payment address")
|
|
num_payment_sigs = 2
|
|
|
|
else:
|
|
num_payment_sigs = 1
|
|
|
|
payment_input_len = 0
|
|
payment_output_len = 0
|
|
|
|
if virtualchain.is_p2sh_address(payment_address):
|
|
payment_input_len = APPROX_TX_IN_P2SH_LEN
|
|
payment_output_len = APPROX_TX_OUT_P2SH_LEN
|
|
else:
|
|
payment_input_len = APPROX_TX_IN_P2PKH_LEN
|
|
payment_output_len = APPROX_TX_IN_P2PKH_LEN
|
|
|
|
# assuming they're p2pkh outputs...
|
|
subsidy_byte_count = APPROX_TX_OVERHEAD_LEN + (num_payment_inputs * (71 + payment_input_len)) + payment_output_len # ~71 bytes for signature
|
|
return subsidy_byte_count
|
|
|
|
|
|
def estimate_owner_output_length( owner_address, owner_num_sigs=None ):
|
|
"""
|
|
Estimate the length of the owner input/output
|
|
of a transaction
|
|
"""
|
|
assert owner_address
|
|
owner_address = str(owner_address)
|
|
|
|
if virtualchain.is_p2sh_address( owner_address ):
|
|
if owner_num_sigs is None:
|
|
log.warning("Guessing that owner address {} requires 2 signatures".format(owner_address))
|
|
owner_num_sigs = 2
|
|
|
|
return APPROX_TX_OVERHEAD_LEN + APPROX_TX_IN_P2SH_LEN + APPROX_TX_OUT_P2SH_LEN
|
|
|
|
else:
|
|
return APPROX_TX_OVERHEAD_LEN + APPROX_TX_IN_P2PKH_LEN + APPROX_TX_OUT_P2PKH_LEN
|
|
|
|
|
|
def subsidize_or_pad_transaction( unsigned_tx, owner_address, owner_privkey_params, payment_privkey_info, fees_func, utxo_client, payment_address=None, config_path=CONFIG_PATH ):
|
|
"""
|
|
Subsidize an unsigned transaction, or append the equivalent
|
|
number of bytes (as 0's).
|
|
|
|
The point is to get a byte string that is long enough.
|
|
|
|
Return the new transaction on success
|
|
Raise Exception if payment_address is None and private key info is None
|
|
"""
|
|
|
|
fake_privkey = make_fake_privkey_info( owner_privkey_params )
|
|
signed_subsidized_tx = None
|
|
|
|
if payment_privkey_info is not None:
|
|
# actually try to subsidize this tx
|
|
subsidized_tx = tx_make_subsidizable( unsigned_tx, fees_func, 21 * 10**14, payment_privkey_info, utxo_client )
|
|
assert subsidized_tx is not None
|
|
|
|
signed_subsidized_tx = sign_tx( subsidized_tx, fake_privkey )
|
|
|
|
# there will be at least one more output here (the registration output), so append that too
|
|
pad_len = estimate_owner_output_length(owner_address)
|
|
signed_subsidized_tx += "00" * pad_len
|
|
|
|
else:
|
|
# do a rough size estimation
|
|
if payment_address is None:
|
|
log.error("BUG: missing payment private key and address")
|
|
raise Exception("Need either payment_privkey_info or payment_address")
|
|
|
|
num_extra_bytes = estimate_payment_bytes( payment_address, utxo_client, config_path=config_path )
|
|
signed_subsidized_tx = unsigned_tx + '00' * num_extra_bytes
|
|
|
|
return signed_subsidized_tx
|
|
|
|
|
|
def estimate_preorder_tx_fee( name, name_cost, owner_address, payment_addr, utxo_client, min_payment_confs=TX_MIN_CONFIRMATIONS, owner_privkey_params=(None, None), config_path=CONFIG_PATH, include_dust=False ):
|
|
"""
|
|
Estimate the transaction fee of a preorder.
|
|
Optionally include the dust fees as well.
|
|
Return the number of satoshis on success
|
|
Return None on error
|
|
"""
|
|
|
|
assert owner_address
|
|
assert payment_addr
|
|
|
|
owner_address = str(owner_address)
|
|
payment_addr = str(payment_addr)
|
|
|
|
fake_consensus_hash = 'd4049672223f42aac2855d2fbf2f38f0'
|
|
|
|
try:
|
|
unsigned_tx = preorder_tx( name, payment_addr, owner_address, name_cost, fake_consensus_hash, utxo_client, min_payment_confs=min_payment_confs )
|
|
assert unsigned_tx
|
|
except ValueError:
|
|
# unfunded payment addr
|
|
unsigned_tx = preorder_tx( name, payment_addr, owner_address, name_cost, fake_consensus_hash, utxo_client, safety=False, subsidize=True, min_payment_confs=min_payment_confs )
|
|
assert unsigned_tx
|
|
|
|
pad_len = estimate_owner_output_length(owner_address)
|
|
unsigned_tx += "00" * pad_len
|
|
|
|
signed_subsidized_tx = subsidize_or_pad_transaction(unsigned_tx, owner_address, owner_privkey_params, None, fees_preorder, utxo_client, payment_address=payment_addr, config_path=config_path )
|
|
|
|
tx_fee = get_tx_fee( signed_subsidized_tx, config_path=config_path )
|
|
if tx_fee is None:
|
|
log.error("Failed to get tx fee")
|
|
return None
|
|
|
|
log.debug("preorder tx %s bytes, %s satoshis" % (len(signed_subsidized_tx)/2, int(tx_fee)))
|
|
|
|
if include_dust:
|
|
dust_fee = estimate_dust_fee( signed_subsidized_tx, fees_preorder )
|
|
assert dust_fee is not None
|
|
log.debug("Additional dust fee: %s" % dust_fee)
|
|
tx_fee += dust_fee
|
|
|
|
return tx_fee
|
|
|
|
|
|
def estimate_register_tx_fee( name, owner_addr, payment_addr, utxo_client, owner_privkey_params=(None, None), config_path=CONFIG_PATH, include_dust=False ):
|
|
"""
|
|
Estimate the transaction fee of a register.
|
|
Optionally include the dust fees as well.
|
|
Return the number of satoshis on success
|
|
Return None on error
|
|
"""
|
|
|
|
assert owner_addr
|
|
assert payment_addr
|
|
|
|
owner_addr = str(owner_addr)
|
|
payment_addr = str(payment_addr)
|
|
|
|
fake_privkey = make_fake_privkey_info( owner_privkey_params )
|
|
|
|
try:
|
|
unsigned_tx = register_tx( name, payment_addr, owner_addr, utxo_client, subsidized=True )
|
|
assert unsigned_tx
|
|
except ValueError:
|
|
# no UTXOs for this owner address. Try again and add padding for one
|
|
unsigned_tx = register_tx( name, payment_addr, owner_addr, utxo_client, subsidized=True, safety=False )
|
|
assert unsigned_tx
|
|
|
|
pad_len = estimate_owner_output_length(owner_addr)
|
|
unsigned_tx += "00" * pad_len
|
|
|
|
signed_subsidized_tx = subsidize_or_pad_transaction(unsigned_tx, owner_addr, owner_privkey_params, None, fees_registration, utxo_client, payment_address=payment_addr, config_path=config_path )
|
|
|
|
tx_fee = get_tx_fee( signed_subsidized_tx, config_path=config_path )
|
|
if tx_fee is None:
|
|
log.error("Failed to get tx fee")
|
|
return None
|
|
|
|
log.debug("register tx %s bytes, %s satoshis txfee" % (len(signed_subsidized_tx)/2, int(tx_fee)))
|
|
|
|
if include_dust:
|
|
dust_fee = estimate_dust_fee( signed_subsidized_tx, fees_registration )
|
|
assert dust_fee is not None
|
|
log.debug("Additional dust fee: %s" % dust_fee)
|
|
tx_fee += dust_fee
|
|
|
|
return tx_fee
|
|
|
|
|
|
def estimate_renewal_tx_fee( name, renewal_fee, payment_privkey_info, owner_privkey_info, utxo_client, config_path=CONFIG_PATH, include_dust=False ):
|
|
"""
|
|
Estimate the transaction fee of a renewal.
|
|
Optionally include the dust fees as well.
|
|
Return the number of satoshis on success
|
|
Return None on error
|
|
"""
|
|
|
|
payment_address = get_privkey_info_address( payment_privkey_info )
|
|
owner_address = get_privkey_info_address( owner_privkey_info )
|
|
owner_privkey_params = get_privkey_info_params(owner_privkey_info)
|
|
|
|
try:
|
|
unsigned_tx = register_tx( name, owner_address, owner_address, utxo_client, renewal_fee=renewal_fee )
|
|
except (AssertionError, ValueError), ve:
|
|
# no UTXOs for this owner address. Try again and add padding for one
|
|
unsigned_tx = register_tx( name, owner_address, owner_address, utxo_client, renewal_fee=renewal_fee, subsidized=True, safety=False )
|
|
assert unsigned_tx
|
|
|
|
pad_len = estimate_owner_output_length(owner_address)
|
|
unsigned_tx += "00" * pad_len
|
|
|
|
signed_subsidized_tx = subsidize_or_pad_transaction(unsigned_tx, owner_address, owner_privkey_params, payment_privkey_info, fees_registration, utxo_client, payment_address=payment_address, config_path=config_path )
|
|
|
|
tx_fee = get_tx_fee( signed_subsidized_tx, config_path=config_path )
|
|
if tx_fee is None:
|
|
log.error("Failed to get tx fee")
|
|
return None
|
|
|
|
log.debug("renewal tx %s bytes, %s satoshis txfee" % (len(signed_subsidized_tx)/2, int(tx_fee)))
|
|
|
|
if include_dust:
|
|
dust_fee = estimate_dust_fee( unsigned_tx, fees_registration ) # must be unsigned_tx, without subsidy
|
|
assert dust_fee is not None
|
|
log.debug("Additional dust fee: %s" % dust_fee)
|
|
tx_fee += dust_fee
|
|
|
|
return tx_fee
|
|
|
|
|
|
def estimate_update_tx_fee( name, payment_privkey_info, owner_address, utxo_client, owner_privkey_params=(None, None), config_path=CONFIG_PATH, payment_address=None, include_dust=False ):
|
|
"""
|
|
Estimate the transaction fee of an update.
|
|
Optionally include the dust fees as well.
|
|
Return the number of satoshis on success
|
|
Return None on error
|
|
"""
|
|
|
|
assert owner_address
|
|
owner_address = str(owner_address)
|
|
|
|
fake_consensus_hash = 'd4049672223f42aac2855d2fbf2f38f0'
|
|
fake_zonefile_hash = '20b512149140494c0f7d565023973226908f6940'
|
|
|
|
fake_privkey = make_fake_privkey_info( owner_privkey_params )
|
|
|
|
signed_subsidized_tx = None
|
|
if payment_privkey_info is not None:
|
|
# consistency
|
|
payment_address = get_privkey_info_address( payment_privkey_info )
|
|
|
|
try:
|
|
unsigned_tx = None
|
|
try:
|
|
unsigned_tx = update_tx( name, fake_zonefile_hash, fake_consensus_hash, owner_address, utxo_client, subsidize=True )
|
|
except AssertionError as ae:
|
|
# no UTXOs for this owner address. Try again and add padding for one
|
|
unsigned_tx = update_tx( name, fake_zonefile_hash, fake_consensus_hash, owner_address, utxo_client, subsidize=True, safety=False )
|
|
assert unsigned_tx
|
|
|
|
pad_len = estimate_owner_output_length(owner_address)
|
|
unsigned_tx += "00" * pad_len
|
|
|
|
signed_subsidized_tx = subsidize_or_pad_transaction(unsigned_tx, owner_address, owner_privkey_params, payment_privkey_info, fees_update, utxo_client, payment_address=payment_address, config_path=config_path )
|
|
|
|
except ValueError as ve:
|
|
if BLOCKSTACK_TEST:
|
|
log.exception(ve)
|
|
print >> sys.stderr, "payment key info: %s" % str(payment_privkey_info)
|
|
|
|
log.error("Insufficient funds: Not enough inputs to make an update transaction.")
|
|
return None
|
|
|
|
except AssertionError as ae:
|
|
if BLOCKSTACK_DEBUG:
|
|
log.exception(ae)
|
|
|
|
log.error("Unable to create transaction")
|
|
return None
|
|
|
|
except Exception as e:
|
|
if os.environ.get("BLOCKSTACK_TEST") == "1":
|
|
log.exception(e)
|
|
|
|
return None
|
|
|
|
tx_fee = get_tx_fee( signed_subsidized_tx, config_path=config_path )
|
|
if tx_fee is None:
|
|
log.error("Failed to get tx fee")
|
|
return None
|
|
|
|
log.debug("update tx %s bytes, %s satoshis txfee" % (len(signed_subsidized_tx)/2, int(tx_fee)))
|
|
|
|
if include_dust:
|
|
dust_fee = estimate_dust_fee( unsigned_tx, fees_update ) # must be unsigned tx, without subsidy
|
|
assert dust_fee is not None
|
|
log.debug("Additional dust fee: %s" % dust_fee)
|
|
tx_fee += dust_fee
|
|
|
|
return tx_fee
|
|
|
|
|
|
def estimate_transfer_tx_fee( name, payment_privkey_info, owner_address, utxo_client, owner_privkey_params=(None, None), payment_address=None, config_path=CONFIG_PATH, include_dust=False ):
|
|
"""
|
|
Estimate the transaction fee of a transfer.
|
|
Optionally include the dust fees as well.
|
|
Return the number of satoshis on success
|
|
Return None on error
|
|
"""
|
|
|
|
assert owner_address
|
|
owner_address = str(owner_address)
|
|
|
|
fake_recipient_address = virtualchain.address_reencode('1LL4X7wNUBCWoDhfVLA2cHE7xk1ZJMT98Q')
|
|
fake_consensus_hash = 'd4049672223f42aac2855d2fbf2f38f0'
|
|
|
|
fake_privkey = make_fake_privkey_info( owner_privkey_params )
|
|
|
|
unsigned_tx = None
|
|
try:
|
|
try:
|
|
unsigned_tx = transfer_tx( name, fake_recipient_address, True, fake_consensus_hash, owner_address, utxo_client, subsidize=True )
|
|
except AssertionError as ae:
|
|
# no UTXOs for this owner address. Try again and add padding for one
|
|
unsigned_tx = transfer_tx( name, fake_recipient_address, True, fake_consensus_hash, owner_address, utxo_client, subsidize=True, safety=False )
|
|
assert unsigned_tx
|
|
|
|
pad_len = estimate_owner_output_length(owner_address)
|
|
unsigned_tx += "00" * pad_len
|
|
|
|
signed_subsidized_tx = subsidize_or_pad_transaction(unsigned_tx, owner_address, owner_privkey_params, payment_privkey_info, fees_transfer, utxo_client, payment_address=payment_address, config_path=config_path )
|
|
|
|
except ValueError, ve:
|
|
if os.environ.get("BLOCKSTACK_TEST") == "1":
|
|
log.exception(ve)
|
|
|
|
log.error("Insufficient funds: Not enough inputs to make a transfer transaction.")
|
|
return None
|
|
|
|
except AssertionError as ae:
|
|
if BLOCKSTACK_DEBUG:
|
|
log.exception(ae)
|
|
|
|
log.error("Unable to make transaction")
|
|
return None
|
|
|
|
except Exception as e:
|
|
if os.environ.get("BLOCKSTACK_TEST") == "1":
|
|
log.exception(e)
|
|
|
|
return None
|
|
|
|
tx_fee = get_tx_fee( signed_subsidized_tx, config_path=config_path )
|
|
if tx_fee is None:
|
|
log.error("Failed to get tx fee")
|
|
return None
|
|
|
|
log.debug("transfer tx %s bytes, %s satoshis txfee" % (len(signed_subsidized_tx)/2, int(tx_fee)))
|
|
|
|
if include_dust:
|
|
dust_fee = estimate_dust_fee( unsigned_tx, fees_transfer ) # must be unsigned tx, without subsidy
|
|
assert dust_fee is not None
|
|
log.debug("Additional dust fee: %s" % dust_fee)
|
|
tx_fee += dust_fee
|
|
|
|
return tx_fee
|
|
|
|
|
|
def estimate_revoke_tx_fee( name, payment_privkey_info, owner_address, utxo_client, owner_privkey_params=(None, None), config_path=CONFIG_PATH, include_dust=False ):
|
|
"""
|
|
Estimate the transaction fee of a revoke.
|
|
Optionally include the dust fees as well.
|
|
Return the number of satoshis on success
|
|
Return None on error
|
|
"""
|
|
|
|
assert owner_address
|
|
owner_address = str(owner_address)
|
|
|
|
fake_privkey = make_fake_privkey_info( owner_privkey_params )
|
|
unsigned_tx = None
|
|
|
|
try:
|
|
try:
|
|
unsigned_tx = revoke_tx( name, owner_address, utxo_client, subsidize=True )
|
|
except AssertionError as ae:
|
|
# no UTXOs for this owner address. Try again and add padding for one
|
|
unsigned_tx = revoke_tx( name, owner_address, utxo_client, subsidize=True, safety=False )
|
|
assert unsigned_tx
|
|
|
|
pad_len = estimate_owner_output_length(owner_address)
|
|
unsigned_tx += "00" * pad_len
|
|
|
|
signed_subsidized_tx = subsidize_or_pad_transaction(unsigned_tx, owner_address, owner_privkey_params, payment_privkey_info, fees_revoke, utxo_client, config_path=config_path )
|
|
|
|
except ValueError, ve:
|
|
if os.environ.get("BLOCKSTACK_TEST") == "1":
|
|
log.exception(ve)
|
|
|
|
log.error("Insufficient funds: Not enough inputs to make a revoke transaction.")
|
|
return None
|
|
|
|
except AssertionError as ae:
|
|
if BLOCKSTACK_DEBUG:
|
|
log.exception(ae)
|
|
|
|
log.error("Unable to make transaction")
|
|
return None
|
|
|
|
except Exception as e:
|
|
if os.environ.get("BLOCKSTACK_TEST") == "1":
|
|
log.exception(e)
|
|
|
|
return None
|
|
|
|
tx_fee = get_tx_fee( signed_subsidized_tx, config_path=config_path )
|
|
if tx_fee is None:
|
|
log.error("Failed to get tx fee")
|
|
return None
|
|
|
|
log.debug("revoke tx %s bytes, %s satoshis txfee" % (len(signed_subsidized_tx)/2, int(tx_fee)))
|
|
|
|
if include_dust:
|
|
dust_fee = estimate_dust_fee( unsigned_tx, fees_revoke ) # must be unsigned tx, without subsidy
|
|
assert dust_fee is not None
|
|
log.debug("Additional dust fee: %s" % dust_fee)
|
|
tx_fee += dust_fee
|
|
|
|
return tx_fee
|
|
|
|
|
|
def estimate_name_import_tx_fee( fqu, payment_addr, utxo_client, config_path=CONFIG_PATH, include_dust=False ):
|
|
"""
|
|
Estimate the transaction fee of a name import.
|
|
Return the number of satoshis on success
|
|
Return None on error
|
|
|
|
TODO: no dust fee estimation available for imports
|
|
"""
|
|
|
|
assert payment_addr
|
|
payment_addr = str(payment_addr)
|
|
|
|
fake_privkey = '5J8V3QacBzCwh6J9NJGZJHQ5NoJtMzmyUgiYFkBEgUzKdbFo7GX' # fake private key (NOTE: NAME_IMPORT only supports p2pkh)
|
|
fake_zonefile_hash = '20b512149140494c0f7d565023973226908f6940'
|
|
fake_recipient_address = virtualchain.address_reencode('1LL4X7wNUBCWoDhfVLA2cHE7xk1ZJMT98Q')
|
|
|
|
try:
|
|
unsigned_tx = name_import_tx( fqu, fake_recipient_address, fake_zonefile_hash, payment_addr, utxo_client )
|
|
signed_tx = sign_tx( unsigned_tx, fake_privkey )
|
|
except ValueError, ve:
|
|
if os.environ.get("BLOCKSTACK_TEST") == "1":
|
|
log.exception(ve)
|
|
|
|
log.error("Insufficient funds: Not enough inputs to make an import transaction")
|
|
return None
|
|
|
|
tx_fee = get_tx_fee( signed_tx, config_path=config_path )
|
|
if tx_fee is None:
|
|
log.error("Failed to get tx fee")
|
|
return None
|
|
|
|
log.debug("name import tx %s bytes, %s satoshis txfee" % (len(signed_tx)/2, int(tx_fee)))
|
|
return tx_fee
|
|
|
|
|
|
def estimate_namespace_preorder_tx_fee( namespace_id, cost, payment_address, utxo_client, config_path=CONFIG_PATH, include_dust=False ):
|
|
"""
|
|
Estimate the transaction fee of a namespace preorder
|
|
Return the number of satoshis on success
|
|
Return None on error
|
|
|
|
TODO: no dust fee estimation available for namespace preorder
|
|
"""
|
|
|
|
assert payment_address
|
|
payment_address = str(payment_address)
|
|
|
|
fake_privkey = virtualchain.BitcoinPrivateKey('5J8V3QacBzCwh6J9NJGZJHQ5NoJtMzmyUgiYFkBEgUzKdbFo7GX').to_hex() # fake private key (NOTE: NAMESPACE_PREORDER only supports p2pkh)
|
|
fake_reveal_address = virtualchain.address_reencode('1LL4X7wNUBCWoDhfVLA2cHE7xk1ZJMT98Q')
|
|
fake_consensus_hash = 'd4049672223f42aac2855d2fbf2f38f0'
|
|
|
|
try:
|
|
unsigned_tx = namespace_preorder_tx( namespace_id, fake_reveal_address, cost, fake_consensus_hash, payment_address, utxo_client )
|
|
signed_tx = sign_tx( unsigned_tx, fake_privkey )
|
|
except ValueError, ve:
|
|
if os.environ.get("BLOCKSTACK_TEST") == "1":
|
|
log.exception(ve)
|
|
|
|
log.error("Insufficient funds: Not enough inputs to make a namespace-preorder transaction.")
|
|
return None
|
|
|
|
tx_fee = get_tx_fee( signed_tx, config_path=config_path )
|
|
if tx_fee is None:
|
|
log.error("Failed to get tx fee")
|
|
return None
|
|
|
|
log.debug("namespace preorder tx %s bytes, %s satoshis" % (len(signed_tx)/2, int(tx_fee)))
|
|
return tx_fee
|
|
|
|
|
|
def estimate_namespace_reveal_tx_fee( namespace_id, payment_address, utxo_client, config_path=CONFIG_PATH, include_dust=False ):
|
|
"""
|
|
Estimate the transaction fee of a namespace reveal
|
|
Return the number of satoshis on success
|
|
Return None on error
|
|
|
|
TODO: no dust estimation available for namespace reveal
|
|
"""
|
|
|
|
assert payment_address
|
|
payment_address = str(payment_address)
|
|
|
|
fake_privkey = virtualchain.BitcoinPrivateKey('5J8V3QacBzCwh6J9NJGZJHQ5NoJtMzmyUgiYFkBEgUzKdbFo7GX').to_hex() # fake private key (NOTE: NAMESPACE_REVEAL only supports p2pkh)
|
|
fake_reveal_address = virtualchain.address_reencode('1LL4X7wNUBCWoDhfVLA2cHE7xk1ZJMT98Q')
|
|
|
|
try:
|
|
unsigned_tx = namespace_reveal_tx( namespace_id, fake_reveal_address, 1, 2, 3, [4,5,6,7,8,9,10,11,12,13,14,15,0,1,2,3], 4, 5, payment_address, utxo_client )
|
|
signed_tx = sign_tx( unsigned_tx, fake_privkey )
|
|
except ValueError, ve:
|
|
if os.environ.get("BLOCKSTACK_TEST") == "1":
|
|
log.exception(ve)
|
|
|
|
log.error("Insufficient funds: Not enough inputs to make a namespace-reveal transaction.")
|
|
return None
|
|
|
|
tx_fee = get_tx_fee( signed_tx, config_path=config_path )
|
|
if tx_fee is None:
|
|
log.error("Failed to get tx fee")
|
|
return None
|
|
|
|
log.debug("namespace reveal tx %s bytes, %s satoshis txfee" % (len(signed_tx)/2, int(tx_fee)))
|
|
|
|
if include_dust:
|
|
dust_fee = estimate_dust_fee( signed_tx, fees_namespace_reveal )
|
|
assert dust_fee is not None
|
|
log.debug("Additional dust fee: %s" % dust_fee)
|
|
tx_fee += dust_fee
|
|
|
|
return tx_fee
|
|
|
|
|
|
def estimate_namespace_ready_tx_fee( namespace_id, reveal_addr, utxo_client, config_path=CONFIG_PATH, include_dust=False ):
|
|
"""
|
|
Estimate the transaction fee of a namespace ready
|
|
Return the number of satoshis on success
|
|
Return None on error
|
|
|
|
TODO: no dust estimation available for namespace ready
|
|
"""
|
|
|
|
assert reveal_addr
|
|
reveal_addr = str(reveal_addr)
|
|
|
|
fake_privkey = virtualchain.BitcoinPrivateKey('5J8V3QacBzCwh6J9NJGZJHQ5NoJtMzmyUgiYFkBEgUzKdbFo7GX').to_hex() # fake private key (NOTE: NAMESPACE_READY only supports p2pkh)
|
|
|
|
try:
|
|
unsigned_tx = namespace_ready_tx( namespace_id, reveal_addr, utxo_client )
|
|
signed_tx = sign_tx( unsigned_tx, fake_privkey )
|
|
except ValueError, ve:
|
|
if os.environ.get("BLOCKSTACK_TEST") == "1":
|
|
log.exception(ve)
|
|
|
|
log.error("Insufficient funds: Not enough inputs to make a namespace-ready transaction.")
|
|
return None
|
|
|
|
tx_fee = get_tx_fee( signed_tx, config_path=config_path )
|
|
if tx_fee is None:
|
|
log.error("Failed to get tx fee")
|
|
return None
|
|
|
|
log.debug("namespace ready tx %s bytes, %s satoshis txfee" % (len(signed_tx)/2, int(tx_fee)))
|
|
|
|
return tx_fee
|
|
|
|
|
|
def estimate_announce_tx_fee( sender_address, utxo_client, sender_privkey_params=(1, 1), config_path=CONFIG_PATH, include_dust=False ):
|
|
"""
|
|
Estimate the transaction fee of an announcement tx
|
|
Return the number of satoshis on success
|
|
Return None on error
|
|
"""
|
|
|
|
assert sender_address
|
|
sender_address = str(sender_address)
|
|
|
|
fake_privkey = make_fake_privkey_info( sender_privkey_params )
|
|
fake_announce_hash = '20b512149140494c0f7d565023973226908f6940'
|
|
|
|
try:
|
|
unsigned_tx = announce_tx( fake_announce_hash, sender_address, utxo_client )
|
|
signed_tx = sign_tx( unsigned_tx, fake_privkey )
|
|
except ValueError, ve:
|
|
if os.environ.get("BLOCKSTACK_TEST") == "1":
|
|
log.exception(ve)
|
|
|
|
log.error("Insufficient funds: Not enough inputs to make an announce transaction.")
|
|
return None
|
|
|
|
tx_fee = get_tx_fee( signed_tx, config_path=config_path )
|
|
if tx_fee is None:
|
|
log.error("Failed to get tx fee")
|
|
return None
|
|
|
|
log.debug("announce tx %s bytes, %s satoshis" % (len(signed_tx)/2, int(tx_fee)))
|
|
|
|
if include_dust:
|
|
dust_fee = estimate_dust_fee( signed_tx, fees_announce )
|
|
assert dust_fee is not None
|
|
log.debug("Additional dust fee: %s" % dust_fee)
|
|
tx_fee += dust_fee
|
|
|
|
return tx_fee
|
|
|
|
|
|
def get_consensus_hash( proxy, config_path=CONFIG_PATH ):
|
|
"""
|
|
Get the current consensus hash from the server.
|
|
Also verify that the server has processed sufficiently
|
|
many blocks (compared to what bitcoind tells us).
|
|
Return {'status': True, 'consensus_hash': ...} on success
|
|
Return {'error': ...} on failure
|
|
"""
|
|
|
|
delay = 1.0
|
|
while True:
|
|
blockstack_info = blockstack_getinfo( proxy=proxy )
|
|
if 'error' in blockstack_info:
|
|
log.error("Blockstack server did not return consensus hash: {}".format(blockstack_info['error']))
|
|
time.sleep(delay)
|
|
delay = 2 * delay + random.randint(0, delay)
|
|
continue
|
|
|
|
# up-to-date?
|
|
last_block_processed = None
|
|
last_block_seen = None
|
|
try:
|
|
last_block_processed = int(blockstack_info['last_block_processed'])
|
|
last_block_seen = int(blockstack_info['last_block_seen'])
|
|
consensus_hash = blockstack_info['consensus']
|
|
except Exception, e:
|
|
log.exception(e)
|
|
log.error("Invalid consensus hash from server")
|
|
time.sleep(delay)
|
|
delay = 2 * delay + random.randint(0, delay)
|
|
continue
|
|
|
|
# valid?
|
|
height = get_block_height( config_path=config_path )
|
|
if height is None:
|
|
log.error("Failed to get blockchain height")
|
|
delay = 2 * delay + random.randint(0, delay)
|
|
continue
|
|
|
|
if height > last_block_processed + 20 or (last_block_seen is not None and last_block_seen > last_block_processed + 20):
|
|
# server is lagging
|
|
log.error("Server is lagging behind: bitcoind height is %s, server is %s" % (height, last_block_processed))
|
|
delay = 2 * delay + random.randint(0, delay)
|
|
|
|
# success
|
|
return {'status': True, 'consensus_hash': consensus_hash}
|
|
|
|
|
|
def address_privkey_match( address, privkey_params ):
|
|
"""
|
|
Does an address correspond to the private key information?
|
|
i.e. singlesig --> p2pkh address
|
|
i.e. multisig --> p2sh address
|
|
"""
|
|
if privkey_params == (1,1) and pybitcoin.b58check_version_byte( str(address) ) != virtualchain.version_byte:
|
|
# invalid address, given parameters
|
|
log.error("Address %s does not correspond to a single private key" % address)
|
|
return False
|
|
|
|
elif (privkey_params[0] > 1 or privkey_params[1] > 1) and pybitcoin.b58check_version_byte( str(address) ) != virtualchain.multisig_version_byte:
|
|
# invalid address
|
|
log.error("Address %s does not correspond to multisig private keys")
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def build_utxo_client( utxo_client=None, utxos=None, address=None ):
|
|
"""
|
|
Build a UTXO client.
|
|
This can be called multiple times with different addresses and UTXO lists.
|
|
|
|
Return the UTXO client instance on success.
|
|
Return None on error
|
|
"""
|
|
if utxo_client:
|
|
if isinstance(utxo_client, UTXOWrapper):
|
|
# append to this client
|
|
if utxos is not None and address is not None:
|
|
utxo_client.add_unspents( address, utxos )
|
|
|
|
return utxo_client
|
|
|
|
if utxos is None or address is None:
|
|
log.error("No payment address or payment UTXO list")
|
|
return None
|
|
|
|
utxo_client = UTXOWrapper()
|
|
utxo_client.add_unspents( address, utxos )
|
|
return utxo_client
|
|
|
|
|
|
def deduce_privkey_params( address=None, privkey_info=None, privkey_params=(None, None) ):
|
|
"""
|
|
Try to figure out what the private key parameters (m, n) are.
|
|
Return (m, n) on success
|
|
Raise AssertionError on failure
|
|
"""
|
|
if privkey_params[0] and privkey_params[1]:
|
|
|
|
if address is not None:
|
|
assert address_privkey_match(address, privkey_params), "Address does not match private key params"
|
|
|
|
if privkey_info is not None:
|
|
assert privkey_params == get_privkey_info_params( privkey_info ), "Params do not match private key"
|
|
|
|
return privkey_params
|
|
|
|
assert address is not None and privkey_info is not None, "Missing both address and private key info"
|
|
|
|
if privkey_info is not None:
|
|
privkey_params = get_privkey_info_params( privkey_info )
|
|
|
|
msg = "Either key or key parameters are required"
|
|
assert privkey_params, msg
|
|
assert privkey_params[0] is not None, msg
|
|
assert privkey_params[1] is not None, msg
|
|
return privkey_params
|
|
|
|
if address is not None:
|
|
assert not virtualchain.is_p2sh_address(address), "Cannot deduce private key params from multisig address"
|
|
return (1, 1)
|
|
|
|
raise AssertionError("Unable to deduce private key params")
|
|
|
|
|
|
def do_blockchain_tx( unsigned_tx, privkey_info=None, config_path=CONFIG_PATH, tx_broadcaster=None, dry_run=False ):
|
|
"""
|
|
Sign and/or broadcast a subsidized transaction.
|
|
If dry_run is True, then don't actually send the transaction (and don't bother signing it if no key is given).
|
|
|
|
Return {'status': True, 'transaction_hash': ...} on successful signing and broadcasting
|
|
Return {'status': True, 'tx': ...} otherwise. 'tx' will be signed if privkey_info is given.
|
|
|
|
Return {'error': ...} on failure
|
|
"""
|
|
|
|
assert privkey_info or dry_run, "Missing payment key"
|
|
|
|
try:
|
|
if dry_run:
|
|
if payment_privkey_info is not None:
|
|
resp = sign_tx( unsigned_tx, privkey_info )
|
|
else:
|
|
resp = unsigned_tx
|
|
|
|
if resp is None:
|
|
resp = {'error': 'Failed to generate signed register tx'}
|
|
else:
|
|
resp = {'status': True, 'tx': resp}
|
|
|
|
else:
|
|
resp = sign_and_broadcast_tx( unsigned_tx, privkey_info, config_path=config_path, tx_broadcaster=tx_broadcaster )
|
|
|
|
return resp
|
|
|
|
except Exception, e:
|
|
if BLOCKSTACK_DEBUG:
|
|
log.exception(e)
|
|
|
|
return {'error': 'Failed to sign and broadcast transaction'}
|
|
|
|
|
|
def do_preorder( fqu, payment_privkey_info, owner_privkey_info, cost_satoshis, utxo_client, tx_broadcaster, tx_fee=None,
|
|
config_path=CONFIG_PATH, owner_address=None, payment_address=None, payment_utxos=None,
|
|
min_payment_confs=TX_MIN_CONFIRMATIONS, proxy=None, consensus_hash=None, dry_run=False, safety_checks=True ):
|
|
"""
|
|
Preorder a name.
|
|
|
|
Either payment_privkey_info or payment_address is necessary.
|
|
Either payment_utxos or utxo_client is necessary.
|
|
|
|
If payment_privkey_info is not given, then dry_run must be true. An unsigned tx will be returned.
|
|
|
|
Return {'status': True, 'transaction_hash': ...} on success (for dry_run = False)
|
|
Return {'status': True, 'tx': ...} on success (for dry_run = True)
|
|
Return {'error': ...} on failure
|
|
"""
|
|
|
|
if proxy is None:
|
|
proxy = get_default_proxy()
|
|
|
|
if dry_run:
|
|
assert tx_fee, "invalid argument: tx_fee is required on dry-run"
|
|
assert cost_satoshis, 'invalid argument: cost_satoshis is required on dry-run'
|
|
safety_checks = False
|
|
|
|
fqu = str(fqu)
|
|
|
|
assert payment_privkey_info or dry_run, "Missing payment private keys"
|
|
assert payment_privkey_info or payment_address, "Missing payment address or keys"
|
|
assert owner_privkey_info or owner_address, "Missing owner address or keys"
|
|
|
|
if owner_address is None:
|
|
owner_address = get_privkey_info_address( owner_privkey_info )
|
|
|
|
if payment_address is None:
|
|
payment_address = get_privkey_info_address( payment_privkey_info )
|
|
|
|
utxo_client = build_utxo_client( utxo_client=utxo_client, address=payment_address, utxos=payment_utxos )
|
|
assert utxo_client, "Unable to build UTXO client"
|
|
|
|
if not dry_run and (safety_checks or (cost_satoshis is None or tx_fee is None)):
|
|
# find tx fee, and do sanity checks
|
|
res = check_preorder(fqu, cost_satoshis, owner_privkey_info, payment_privkey_info, config_path=config_path, proxy=proxy)
|
|
if 'error' in res and safety_checks:
|
|
log.error("Failed to check preorder: {}".format(res['error']))
|
|
return res
|
|
|
|
if tx_fee is None:
|
|
tx_fee = res['tx_fee']
|
|
|
|
if cost_satoshis is None:
|
|
cost_satoshis = res['name_price']
|
|
|
|
assert tx_fee, "Missing tx fee"
|
|
assert cost_satoshis, "Missing name cost"
|
|
|
|
if consensus_hash is None:
|
|
consensus_hash_res = get_consensus_hash( proxy, config_path=config_path )
|
|
if 'error' in consensus_hash_res:
|
|
return {'error': 'Failed to get consensus hash: %s' % consensus_hash_res['error']}
|
|
|
|
consensus_hash = consensus_hash_res['consensus_hash']
|
|
else:
|
|
log.warn("Using user-supplied consensus hash %s" % consensus_hash)
|
|
|
|
log.debug("Preordering (%s, %s, %s), for %s, tx_fee = %s" % (fqu, payment_address, owner_address, cost_satoshis, tx_fee))
|
|
|
|
try:
|
|
unsigned_tx = preorder_tx( fqu, payment_address, owner_address, cost_satoshis, consensus_hash, utxo_client, tx_fee=tx_fee, min_payment_confs=min_payment_confs )
|
|
except ValueError as ve:
|
|
if BLOCKSTACK_DEBUG:
|
|
log.exception(ve)
|
|
|
|
log.error("Failed to create preorder TX")
|
|
return {'error': 'Insufficient funds'}
|
|
|
|
resp = do_blockchain_tx( unsigned_tx, privkey_info=payment_privkey_info, tx_broadcaster=tx_broadcaster, config_path=config_path, dry_run=dry_run )
|
|
return resp
|
|
|
|
|
|
def do_register( fqu, payment_privkey_info, owner_privkey_info, utxo_client, tx_broadcaster, tx_fee=None,
|
|
config_path=CONFIG_PATH, owner_address=None, payment_address=None, payment_utxos=None,
|
|
proxy=None, dry_run=False, safety_checks=True ):
|
|
|
|
"""
|
|
Register a name
|
|
|
|
payment_privkey_info or payment_address is required.
|
|
utxo_client or payment_utxos is required.
|
|
|
|
Return {'status': True, 'transaction_hash': ...} on success
|
|
Return {'status': True, 'tx': ...} if no private key is given, or dry_run is True.
|
|
Return {'error': ...} on failure
|
|
"""
|
|
if proxy is None:
|
|
proxy = get_default_proxy()
|
|
|
|
fqu = str(fqu)
|
|
resp = {}
|
|
|
|
if dry_run:
|
|
assert tx_fee, "Missing tx fee on dry-run"
|
|
safety_checks = False
|
|
|
|
assert payment_privkey_info or dry_run, "Missing payment private keys"
|
|
assert payment_privkey_info or payment_address, "Missing payment address or keys"
|
|
|
|
if payment_address is None:
|
|
payment_address = get_privkey_info_address( payment_privkey_info )
|
|
|
|
if owner_address is None:
|
|
owner_address = get_privkey_info_address( owner_privkey_info )
|
|
|
|
assert payment_privkey_info or (payment_address and payment_utxos), "Missing payment keys or payment UTXOs and address"
|
|
|
|
utxo_client = build_utxo_client( utxo_client=utxo_client, address=payment_address, utxos=payment_utxos )
|
|
assert utxo_client, "Unable to build UTXO client"
|
|
|
|
if not dry_run and (safety_checks or tx_fee is None):
|
|
# find tx fee, and do sanity checks
|
|
res = check_register(fqu, owner_privkey_info, payment_privkey_info, config_path=config_path, proxy=proxy)
|
|
if 'error' in res and safety_checks:
|
|
log.error("Failed to check register: {}".format(res['error']))
|
|
return res
|
|
|
|
if tx_fee is None:
|
|
tx_fee = res['tx_fee']
|
|
|
|
assert tx_fee, "Missing tx fee"
|
|
|
|
log.debug("Registering (%s, %s, %s), tx_fee = %s" % (fqu, payment_address, owner_address, tx_fee))
|
|
|
|
# now send it
|
|
try:
|
|
unsigned_tx = register_tx( fqu, payment_address, owner_address, utxo_client, tx_fee=tx_fee )
|
|
except ValueError as ve:
|
|
if BLOCKSTACK_TEST:
|
|
log.exception(ve)
|
|
|
|
log.error("Failed to create register TX")
|
|
return {'error': 'Insufficient funds'}
|
|
|
|
resp = do_blockchain_tx( unsigned_tx, privkey_info=payment_privkey_info, tx_broadcaster=tx_broadcaster, config_path=config_path, dry_run=dry_run )
|
|
return resp
|
|
|
|
|
|
def do_update( fqu, zonefile_hash, owner_privkey_info, payment_privkey_info, utxo_client, tx_broadcaster, tx_fee=None,
|
|
owner_address=None, owner_utxos=None, payment_address=None, payment_utxos=None,
|
|
config_path=CONFIG_PATH, proxy=None, consensus_hash=None, dry_run=False, safety_checks=True ):
|
|
"""
|
|
Put a new zonefile hash for a name.
|
|
|
|
utxo_client must be given, or UTXO lists for both owner and payment private keys must be given.
|
|
If private key(s) are missing, then dry_run must be True.
|
|
|
|
Return {'status': True, 'transaction_hash': ..., 'value_hash': ...} on success (if dry_run is False)
|
|
return {'status': True, 'tx': ..., 'value_hash': ...} on success (if dry_run is True)
|
|
Return {'error': ...} on failure
|
|
"""
|
|
|
|
if proxy is None:
|
|
proxy = get_default_proxy()
|
|
|
|
if dry_run:
|
|
assert tx_fee, 'dry run needs tx fee'
|
|
safety_checks = False
|
|
|
|
fqu = str(fqu)
|
|
|
|
assert payment_privkey_info or dry_run, "Missing payment private keys"
|
|
assert owner_privkey_info or dry_run, "Missing owner private keys"
|
|
assert payment_privkey_info or payment_address, "Missing payment address or keys"
|
|
assert owner_privkey_info or owner_address, "Missing owner address or keys"
|
|
|
|
if owner_address is None:
|
|
owner_address = get_privkey_info_address( owner_privkey_info )
|
|
|
|
if payment_address is None:
|
|
payment_address = get_privkey_info_address( payment_privkey_info )
|
|
|
|
assert payment_privkey_info or (payment_address and payment_utxos), "Missing payment keys or payment UTXOs and address"
|
|
|
|
# build up UTXO client
|
|
utxo_client = build_utxo_client( utxo_client=utxo_client, address=payment_address, utxos=payment_utxos )
|
|
assert utxo_client, "Unable to build UTXO client"
|
|
|
|
utxo_client = build_utxo_client( utxo_client=utxo_client, address=owner_address, utxos=owner_utxos )
|
|
assert utxo_client, "Unable to build UTXO client"
|
|
|
|
if not dry_run and (safety_checks or tx_fee is None):
|
|
# find tx fee, and do sanity checks
|
|
res = check_update(fqu, owner_privkey_info, payment_privkey_info, config_path=config_path, proxy=proxy)
|
|
if 'error' in res and safety_checks:
|
|
log.error("Failed to check update: {}".format(res['error']))
|
|
return res
|
|
|
|
if tx_fee is None:
|
|
tx_fee = res['tx_fee']
|
|
|
|
assert tx_fee, "Missing tx fee"
|
|
|
|
# get consensus hash
|
|
if consensus_hash is None:
|
|
consensus_hash_res = get_consensus_hash( proxy, config_path=config_path )
|
|
if 'error' in consensus_hash_res:
|
|
log.error("Failed to get consensus hash")
|
|
return {'error': 'Failed to get consensus hash: %s' % consensus_hash_res['error']}
|
|
|
|
consensus_hash = consensus_hash_res['consensus_hash']
|
|
|
|
else:
|
|
log.warn("Using caller-supplied consensus hash '%s'" % consensus_hash)
|
|
|
|
log.debug("Updating (%s, %s)" % (fqu, zonefile_hash))
|
|
log.debug("<owner, payment> (%s, %s) tx_fee = %s" % (owner_address, payment_address, tx_fee))
|
|
|
|
unsigned_tx = None
|
|
try:
|
|
unsigned_tx = update_tx( fqu, zonefile_hash, consensus_hash, owner_address, utxo_client, subsidize=True, tx_fee=tx_fee )
|
|
except ValueError, ve:
|
|
log.exception(ve)
|
|
log.error("Failed to generate update TX")
|
|
return {'error': 'Insufficient funds'}
|
|
except Exception, e:
|
|
log.exception(e)
|
|
return {'error': 'Failed to generate update transaction'}
|
|
|
|
if payment_privkey_info is not None:
|
|
# will subsidize
|
|
try:
|
|
subsidized_tx = tx_make_subsidizable( unsigned_tx, fees_update, 21 * (10**6) * (10**8), payment_privkey_info, utxo_client, tx_fee=tx_fee )
|
|
assert subsidized_tx is not None
|
|
|
|
unsigned_tx = subsidized_tx
|
|
except ValueError, ve:
|
|
if BLOCKSTACK_DEBUG:
|
|
log.exception(ve)
|
|
|
|
log.error("Failed to subsidize update TX")
|
|
return {'error': 'Insufficient funds'}
|
|
except AssertionError as ae:
|
|
if BLOCKSTACK_DEBUG:
|
|
log.exception(ae)
|
|
|
|
log.error("Failed to create subsidized tx")
|
|
return {'error': 'Unable to create transaction'}
|
|
|
|
resp = do_blockchain_tx( unsigned_tx, privkey_info=owner_privkey_info, tx_broadcaster=tx_broadcaster, config_path=config_path, dry_run=dry_run )
|
|
if 'error' in resp:
|
|
return resp
|
|
|
|
resp['value_hash'] = zonefile_hash
|
|
return resp
|
|
|
|
|
|
def do_transfer( fqu, transfer_address, keep_data, owner_privkey_info, payment_privkey_info, utxo_client, tx_broadcaster, tx_fee=None,
|
|
config_path=CONFIG_PATH, proxy=None, consensus_hash=None, dry_run=False, safety_checks=True ):
|
|
"""
|
|
Transfer a name to a new address
|
|
Return {'status': True, 'transaction_hash': ...} on success
|
|
Return {'error': ...} on failure
|
|
"""
|
|
|
|
if proxy is None:
|
|
proxy = get_default_proxy()
|
|
|
|
if dry_run:
|
|
assert tx_fee is not None, 'Need tx fee for dry run'
|
|
safety_checks = False
|
|
|
|
fqu = str(fqu)
|
|
owner_address = get_privkey_info_address(owner_privkey_info)
|
|
payment_address = get_privkey_info_address(payment_privkey_info)
|
|
|
|
if not dry_run and (safety_checks or tx_fee is None):
|
|
# find tx fee, and do sanity checks
|
|
res = check_transfer(fqu, transfer_address, owner_privkey_info, payment_privkey_info, config_path=config_path, proxy=proxy)
|
|
if 'error' in res and safety_checks:
|
|
log.error("Failed to check transfer: {}".format(res['error']))
|
|
return res
|
|
|
|
if tx_fee is None:
|
|
tx_fee = res['tx_fee']
|
|
|
|
assert tx_fee, "Missing tx fee"
|
|
|
|
# get consensus hash
|
|
if consensus_hash is None:
|
|
consensus_hash_res = get_consensus_hash( proxy, config_path=config_path )
|
|
if 'error' in consensus_hash_res:
|
|
return {'error': 'Failed to get consensus hash: %s' % consensus_hash_res['error']}
|
|
|
|
consensus_hash = consensus_hash_res['consensus_hash']
|
|
|
|
else:
|
|
log.warn("Using caller-supplied consensus hash '%s'" % consensus_hash)
|
|
|
|
subsidized_tx = None
|
|
try:
|
|
unsigned_tx = transfer_tx( fqu, transfer_address, keep_data, consensus_hash, owner_address, utxo_client, subsidize=True, tx_fee=tx_fee )
|
|
subsidized_tx = tx_make_subsidizable( unsigned_tx, fees_transfer, 21 * (10**6) * (10**8), payment_privkey_info, utxo_client, tx_fee=tx_fee )
|
|
assert subsidized_tx is not None
|
|
except ValueError as ve:
|
|
if BLOCKSTACK_DEBUG:
|
|
log.exception(ve)
|
|
|
|
log.error("Failed to generate transfer tx")
|
|
return {'error': 'Insufficient funds'}
|
|
except AssertionError as ae:
|
|
if BLOCKSTACK_DEBUG:
|
|
log.exception(ae)
|
|
|
|
log.error("Failed to subsidize transfer tx")
|
|
return {'error': 'Unable to create transaction'}
|
|
|
|
log.debug("Transferring (%s, %s)" % (fqu, transfer_address))
|
|
log.debug("<owner, payment> (%s, %s) tx_fee = %s" % (owner_address, payment_address, tx_fee))
|
|
|
|
resp = do_blockchain_tx( subsidized_tx, privkey_info=owner_privkey_info, tx_broadcaster=tx_broadcaster, config_path=config_path, dry_run=dry_run )
|
|
return resp
|
|
|
|
|
|
def do_renewal( fqu, owner_privkey_info, payment_privkey_info, renewal_fee, utxo_client, tx_broadcaster, tx_fee=None, config_path=CONFIG_PATH, proxy=None, dry_run=False, safety_checks=True ):
|
|
"""
|
|
Renew a name
|
|
Return {'status': True, 'transaction_hash': ...} on success
|
|
Return {'error': ...} on failure
|
|
"""
|
|
if proxy is None:
|
|
proxy = get_default_proxy()
|
|
|
|
if dry_run:
|
|
assert tx_fee, 'Need tx fee for dry run'
|
|
assert renewal_fee, 'Need renewal fee for dry run'
|
|
safety_checks = False
|
|
|
|
fqu = str(fqu)
|
|
resp = {}
|
|
owner_address = get_privkey_info_address( owner_privkey_info )
|
|
payment_address = get_privkey_info_address( payment_privkey_info )
|
|
|
|
if not dry_run and (safety_checks or (renewal_fee is None or tx_fee is None)):
|
|
# find tx fee, and do sanity checks
|
|
res = check_renewal(fqu, renewal_fee, owner_privkey_info, payment_privkey_info, config_path=config_path, proxy=proxy)
|
|
if 'error' in res and safety_checks:
|
|
log.error("Failed to check renewal: {}".format(res['error']))
|
|
return res
|
|
|
|
if tx_fee is None:
|
|
tx_fee = res['tx_fee']
|
|
|
|
if renewal_fee is None:
|
|
renewal_fee = res['name_price']
|
|
|
|
assert tx_fee, "Missing tx fee"
|
|
assert renewal_fee, "Missing renewal fee"
|
|
|
|
log.debug("Renewing (%s, %s, %s), tx_fee = %s, renewal_fee = %s" % (fqu, payment_address, owner_address, tx_fee, renewal_fee))
|
|
|
|
# now send it
|
|
subsidized_tx = None
|
|
try:
|
|
unsigned_tx = register_tx( fqu, owner_address, owner_address, utxo_client, renewal_fee=renewal_fee, tx_fee=tx_fee )
|
|
subsidized_tx = tx_make_subsidizable( unsigned_tx, fees_registration, 21 ** (10**6) * (10**8), payment_privkey_info, utxo_client, tx_fee=tx_fee )
|
|
assert subsidized_tx is not None
|
|
except ValueError as ve:
|
|
if BLOCKSTACK_DEBUG:
|
|
log.exception(ve)
|
|
|
|
log.error("Failed to generate renewal tx")
|
|
return {'error': 'Insufficient funds'}
|
|
except AssertionError as ae:
|
|
if BLOCKSTACK_DEBUG:
|
|
log.exception(ae)
|
|
|
|
log.error("Failed to subsidize renewal tx")
|
|
return {'error': 'Unable to create transaction'}
|
|
|
|
resp = do_blockchain_tx( subsidized_tx, privkey_info=owner_privkey_info, tx_broadcaster=tx_broadcaster, config_path=config_path, dry_run=dry_run )
|
|
return resp
|
|
|
|
|
|
def do_revoke( fqu, owner_privkey_info, payment_privkey_info, utxo_client, tx_broadcaster, config_path=CONFIG_PATH, tx_fee=None, proxy=None, dry_run=False, safety_checks=True):
|
|
"""
|
|
Revoke a name
|
|
Return {'status': True, 'transaction_hash': ...} on success
|
|
Return {'error': ...} on failure
|
|
"""
|
|
if proxy is None:
|
|
proxy = get_default_proxy()
|
|
|
|
if dry_run:
|
|
assert tx_fee, "need tx fee for dry run"
|
|
safety_checks = False
|
|
|
|
fqu = str(fqu)
|
|
owner_address = get_privkey_info_address(owner_privkey_info)
|
|
payment_address = get_privkey_info_address(payment_privkey_info)
|
|
|
|
if not dry_run and (safety_checks or tx_fee is None):
|
|
res = check_revoke(fqu, owner_privkey_info, payment_privkey_info, config_path=config_path, proxy=proxy)
|
|
if 'error' in res and safety_checks:
|
|
log.error("Failed to check revoke: {}".format(res['error']))
|
|
return res
|
|
|
|
if tx_fee is None:
|
|
tx_fee = res['tx_fee']
|
|
|
|
assert tx_fee, "Missing tx fee"
|
|
|
|
subsidized_tx = None
|
|
try:
|
|
unsigned_tx = revoke_tx( fqu, owner_address, utxo_client, subsidize=True, tx_fee=tx_fee )
|
|
subsidized_tx = tx_make_subsidizable( unsigned_tx, fees_revoke, 21 ** (10**6) * (10**8), payment_privkey_info, utxo_client, tx_fee=tx_fee )
|
|
assert subsidized_tx is not None
|
|
except ValueError as ve:
|
|
if BLOCKSTACK_DEBUG:
|
|
log.exception(ve)
|
|
|
|
log.error("Failed to generate revoke tx")
|
|
return {'error': 'Insufficient funds'}
|
|
except AssertionError as ae:
|
|
if BLOCKSTACK_DEBUG:
|
|
log.exception(ae)
|
|
|
|
log.error("Failed to subsidize revoke tx")
|
|
return {'error': 'Unable to create transaction'}
|
|
|
|
log.debug("Revoking %s" % fqu)
|
|
log.debug("<owner, payment> (%s, %s) tx_fee = %s" % (owner_address, payment_address, tx_fee))
|
|
|
|
resp = do_blockchain_tx( subsidized_tx, privkey_info=owner_privkey_info, tx_broadcaster=tx_broadcaster, config_path=config_path, dry_run=dry_run )
|
|
return resp
|
|
|
|
|
|
def do_name_import( fqu, importer_privkey_info, recipient_address, zonefile_hash, utxo_client, tx_broadcaster, config_path=CONFIG_PATH, proxy=None, safety_checks=True, dry_run=False ):
|
|
"""
|
|
Import a name
|
|
Return {'status': True, 'transaction_hash': ..., 'value_hash': ...} on success
|
|
Return {'error': ...} on failure
|
|
"""
|
|
if proxy is None:
|
|
proxy = get_default_proxy()
|
|
|
|
fqu = str(fqu)
|
|
payment_address = None
|
|
|
|
try:
|
|
payment_address = virtualchain.BitcoinPrivateKey( importer_privkey_info ).public_key().address()
|
|
except Exception, e:
|
|
log.exception(e)
|
|
return {'error': 'Import can only use a single private key with a P2PKH script'}
|
|
|
|
log.debug("Import {} with {}".format(fqu, payment_address))
|
|
|
|
tx_fee = estimate_name_import_tx_fee( fqu, payment_address, utxo_client, config_path=config_path )
|
|
if tx_fee is None:
|
|
log.error("Failed to estimate name import tx fee")
|
|
return {'error': 'Failed to get fee estimate. Please check your network settings and verify that you have sufficient funds.'}
|
|
|
|
unsigned_tx = None
|
|
try:
|
|
unsigned_tx = name_import_tx( fqu, recipient_address, zonefile_hash, payment_address, utxo_client, tx_fee=tx_fee )
|
|
except ValueError, ve:
|
|
if BLOCKSTACK_DEBUG:
|
|
log.exception(ve)
|
|
log.error("Failed to generate name import tx")
|
|
return {'error': 'Insufficient funds'}
|
|
|
|
resp = do_blockchain_tx( unsigned_tx, privkey_info=importer_privkey_info, tx_broadcaster=tx_broadcaster, config_path=config_path, dry_run=dry_run )
|
|
if 'error' in resp:
|
|
return resp
|
|
|
|
resp['value_hash'] = zonefile_hash
|
|
return resp
|
|
|
|
|
|
def do_namespace_preorder( namespace_id, cost, payment_privkey_info, reveal_address, utxo_client, tx_broadcaster, consensus_hash=None, config_path=CONFIG_PATH, proxy=None, safety_checks=True, dry_run=False ):
|
|
"""
|
|
Preorder a namespace
|
|
Return {'status': True, 'transaction_hash': ...} on success
|
|
Return {'error': ...} on failure
|
|
"""
|
|
if proxy is None:
|
|
proxy = get_default_proxy()
|
|
|
|
fqu = str(namespace_id)
|
|
payment_address = None
|
|
|
|
try:
|
|
payment_address = virtualchain.BitcoinPrivateKey( payment_privkey_info ).public_key().address()
|
|
except Exception, e:
|
|
log.error("Invalid private key info")
|
|
return {'error': 'Namespace preorder can only use a single private key with a P2PKH script'}
|
|
|
|
# get consensus hash
|
|
if consensus_hash is None:
|
|
consensus_hash_res = get_consensus_hash( proxy, config_path=config_path )
|
|
if 'error' in consensus_hash_res:
|
|
return {'error': 'Failed to get consensus hash: %s' % consensus_hash_res['error']}
|
|
|
|
consensus_hash = consensus_hash_res['consensus_hash']
|
|
|
|
else:
|
|
log.warn("Using caller-supplied consensus hash '%s'" % consensus_hash)
|
|
|
|
if safety_checks:
|
|
# namespace must not exist
|
|
blockchain_record = blockstack_get_namespace_blockchain_record( namespace_id, proxy=proxy )
|
|
if blockchain_record is None or 'error' in blockchain_record:
|
|
if blockchain_record is None:
|
|
log.error("Failed to read blockchain record for %s" % namespace_id)
|
|
return {'error': 'Failed to read blockchain record for namespace'}
|
|
|
|
if blockchain_record['error'] != 'No such namespace':
|
|
log.error("Failed to read blockchain record for %s" % namespace_id)
|
|
return {'error': 'Failed to read blockchain record for namespace'}
|
|
|
|
else:
|
|
# exists
|
|
return {'error': 'Namespace already exists'}
|
|
|
|
tx_fee = estimate_namespace_preorder_tx_fee( namespace_id, cost, payment_address, utxo_client, config_path=config_path )
|
|
if tx_fee is None:
|
|
log.error("Failed to estimate namespace preorder tx fee")
|
|
return {'error': 'Failed to get fee estimate. Please check your network settings and verify that you have sufficient funds.'}
|
|
|
|
log.debug("Preordering namespace (%s, %s, %s), tx_fee = %s" % (namespace_id, payment_address, reveal_address, tx_fee))
|
|
|
|
unsigned_tx = None
|
|
try:
|
|
unsigned_tx = namespace_preorder_tx( namespace_id, reveal_address, cost, consensus_hash, payment_address, utxo_client, tx_fee=tx_fee )
|
|
except ValueError, ve:
|
|
if os.environ.get("BLOCKSTACK_DEBUG") == "1":
|
|
log.exception(ve)
|
|
|
|
log.error("Failed to create namespace preorder tx")
|
|
return {'error': 'Insufficient funds'}
|
|
|
|
resp = do_blockchain_tx( unsigned_tx, privkey_info=payment_privkey_info, tx_broadcaster=tx_broadcaster, config_path=config_path, dry_run=dry_run )
|
|
return resp
|
|
|
|
|
|
def do_namespace_reveal( namespace_id, reveal_address, lifetime, coeff, base_cost, bucket_exponents, nonalpha_discount, no_vowel_discount, payment_privkey_info, utxo_client, tx_broadcaster, config_path=CONFIG_PATH, proxy=None, safety_checks=True, dry_run=False ):
|
|
"""
|
|
Reveal a namespace
|
|
Return {'status': True, 'transaction_hash': ...} on success
|
|
Return {'error': ...} on failure
|
|
"""
|
|
if proxy is None:
|
|
proxy = get_default_proxy()
|
|
|
|
fqu = str(namespace_id)
|
|
payment_address = None
|
|
|
|
try:
|
|
payment_address = virtualchain.BitcoinPrivateKey( payment_privkey_info ).public_key().address()
|
|
except:
|
|
log.error("Invalid private key info")
|
|
return {'error': 'Namespace reveal can only use a single private key with a P2PKH script'}
|
|
|
|
if safety_checks:
|
|
# namespace must not exist
|
|
blockchain_record = blockstack_get_namespace_blockchain_record( namespace_id, proxy=proxy )
|
|
if blockchain_record is None or 'error' in blockchain_record:
|
|
if blockchain_record['error'] != 'No such namespace':
|
|
log.error("Failed to read blockchain record for %s" % namespace_id)
|
|
return {'error': 'Failed to read blockchain record for namespace'}
|
|
|
|
else:
|
|
# exists
|
|
log.error("Namespace already exists")
|
|
return {'error': 'Namespace already exists'}
|
|
|
|
tx_fee = estimate_namespace_reveal_tx_fee( namespace_id, payment_address, utxo_client, config_path=config_path )
|
|
if tx_fee is None:
|
|
log.error("Failed to estimate namespace reveal tx fee")
|
|
return {'error': 'Failed to get fee estimate. Please check your network settings and verify that you have sufficient funds.'}
|
|
|
|
log.debug("Revealing namespace (%s, %s, %s), tx_fee = %s" % (namespace_id, payment_address, reveal_address, tx_fee))
|
|
|
|
try:
|
|
unsigned_tx = namespace_reveal_tx( namespace_id, reveal_address, lifetime, coeff, base_cost, bucket_exponents, nonalpha_discount, no_vowel_discount, payment_address, utxo_client, tx_fee=tx_fee )
|
|
except ValueError, ve:
|
|
if os.environ.get("BLOCKSTACK_TEST") == "1":
|
|
log.exception(ve)
|
|
|
|
return {'error': 'Insufficient funds'}
|
|
|
|
resp = do_blockchain_tx( unsigned_tx, privkey_info=payment_privkey_info, tx_broadcaster=tx_broadcaster, config_path=config_path, dry_run=dry_run )
|
|
return resp
|
|
|
|
|
|
def do_namespace_ready( namespace_id, reveal_privkey_info, utxo_client, tx_broadcaster, config_path=CONFIG_PATH, proxy=None, safety_checks=True, dry_run=False ):
|
|
"""
|
|
Open a namespace for registration
|
|
Return {'status': True, 'transaction_hash': ...} on success
|
|
Return {'error': ...} on failure
|
|
"""
|
|
|
|
if proxy is None:
|
|
proxy = get_default_proxy()
|
|
|
|
fqu = str(namespace_id)
|
|
reveal_address = None
|
|
|
|
try:
|
|
reveal_address = virtualchain.BitcoinPrivateKey( reveal_privkey_info ).public_key().address()
|
|
except:
|
|
log.error("Invalid private key info")
|
|
return {'error': 'Namespace ready can only use a single private key with a P2PKH script'}
|
|
|
|
if safety_checks:
|
|
# namespace must exist, but not be ready
|
|
blockchain_record = blockstack_get_namespace_blockchain_record( namespace_id, proxy=proxy )
|
|
if blockchain_record is None or 'error' in blockchain_record:
|
|
log.error("Failed to read blockchain record for %s" % namespace_id)
|
|
return {'error': 'Failed to read blockchain record for namespace'}
|
|
|
|
if blockchain_record['ready']:
|
|
# exists
|
|
log.error("Namespace already made ready")
|
|
return {'error': 'Namespace already made ready'}
|
|
|
|
tx_fee = estimate_namespace_ready_tx_fee( namespace_id, reveal_address, utxo_client, config_path=config_path )
|
|
if tx_fee is None:
|
|
log.error("Failed to estimate namespace-ready tx fee")
|
|
return {'error': 'Failed to get fee estimate. Please check your network settings and verify that you have sufficient funds.'}
|
|
|
|
log.debug("Readying namespace (%s, %s), tx_fee = %s" % (namespace_id, reveal_address, tx_fee) )
|
|
|
|
try:
|
|
unsigned_tx = namespace_ready_tx( namespace_id, reveal_address, utxo_client, tx_fee=tx_fee )
|
|
except ValueError, ve:
|
|
if os.environ.get("BLOCKSTACK_DEBUG") == "1":
|
|
log.exception(ve)
|
|
|
|
log.error("Failed to create namespace-ready tx")
|
|
return {'error': 'Insufficient funds'}
|
|
|
|
resp = do_blockchain_tx( unsigned_tx, privkey_info=reveal_privkey_info, tx_broadcaster=tx_broadcaster, config_path=config_path, dry_run=dry_run )
|
|
return resp
|
|
|
|
|
|
def do_announce( message_text, sender_privkey_info, utxo_client, tx_broadcaster, config_path=CONFIG_PATH, proxy=None, safety_checks=True, dry_run=False ):
|
|
"""
|
|
Send an announcement hash to the blockchain
|
|
Return {'status': True, 'transaction_hash': ...} on success
|
|
Return {'error': ...} on failure
|
|
"""
|
|
|
|
if proxy is None:
|
|
proxy = get_default_proxy()
|
|
|
|
message_text = str(message_text)
|
|
message_hash = get_blockchain_compat_hash( message_text )
|
|
|
|
sender_address = get_privkey_info_address( sender_privkey_info )
|
|
sender_privkey_params = get_privkey_info_params( sender_privkey_info )
|
|
if sender_privkey_params == (None, None):
|
|
log.error("Invalid owner private key info")
|
|
return {'error': 'Invalid owner private key'}
|
|
|
|
tx_fee = estimate_announce_tx_fee( sender_address, utxo_client, config_path=config_path )
|
|
if tx_fee is None:
|
|
log.error("Failed to estimate announce tx fee")
|
|
return {'error': 'Failed to get fee estimate. Please check your network settings and verify that you have sufficient funds.'}
|
|
|
|
log.debug("Announce (%s, %s) tx_fee = %s" % (message_hash, sender_address, tx_fee))
|
|
|
|
try:
|
|
unsigned_tx = announce_tx( message_hash, sender_address, utxo_client, tx_fee=tx_fee )
|
|
except ValueError, ve:
|
|
if os.environ.get("BLOCKSTACK_DEBUG") == "1":
|
|
log.exception(ve)
|
|
|
|
log.error("Failed to create announce tx")
|
|
return {'error': 'Insufficient funds'}
|
|
|
|
resp = do_blockchain_tx( unsigned_tx, privkey_info=sender_privkey_info, tx_broadcaster=tx_broadcaster, config_path=config_path, dry_run=dry_run )
|
|
if 'error' in resp:
|
|
return resp
|
|
|
|
# only tx?
|
|
if dry_run:
|
|
return resp
|
|
|
|
# stash the announcement text
|
|
res = put_announcement( message_text, resp['transaction_hash'] )
|
|
if 'error' in res:
|
|
log.error("Failed to store announcement text: %s" % res['error'])
|
|
return {'error': 'Failed to store message text', 'transaction_hash': resp['transaction_hash'], 'message_hash': message_hash}
|
|
|
|
else:
|
|
resp['message_hash'] = message_hash
|
|
return resp
|
|
|
|
|
|
|
|
def async_preorder(fqu, payment_privkey_info, owner_privkey_info, cost, tx_fee=None, name_data={}, min_payment_confs=TX_MIN_CONFIRMATIONS,
|
|
proxy=None, config_path=CONFIG_PATH, queue_path=DEFAULT_QUEUE_PATH ):
|
|
"""
|
|
Preorder a fqu (step #1)
|
|
|
|
@fqu: fully qualified name e.g., muneeb.id
|
|
@payment_privkey_info: private key that will pay
|
|
@owner_address: will own the name
|
|
|
|
@transfer_address: will ultimately receive the name
|
|
@zonefile_data: serialized zonefile for the name
|
|
@profile: profile for the name
|
|
|
|
Returns True/False and stores tx_hash in queue
|
|
"""
|
|
|
|
if proxy is None:
|
|
proxy = get_default_proxy(config_path=config_path)
|
|
|
|
utxo_client = get_utxo_provider_client( config_path=config_path )
|
|
tx_broadcaster = get_tx_broadcaster( config_path=config_path )
|
|
|
|
owner_address = get_privkey_info_address( owner_privkey_info )
|
|
payment_address = get_privkey_info_address( payment_privkey_info )
|
|
|
|
# stale preorder will get removed from preorder_queue
|
|
if in_queue("register", fqu, path=queue_path):
|
|
log.error("Already in register queue: %s" % fqu)
|
|
return {'error': 'Already in register queue'}
|
|
|
|
if in_queue("preorder", fqu, path=queue_path):
|
|
log.error("Already in preorder queue: %s" % fqu)
|
|
return {'error': 'Already in preorder queue'}
|
|
|
|
try:
|
|
resp = do_preorder( fqu, payment_privkey_info, owner_privkey_info, cost, utxo_client, tx_broadcaster,
|
|
tx_fee=tx_fee, min_payment_confs=min_payment_confs, config_path=CONFIG_PATH )
|
|
|
|
except Exception, e:
|
|
log.exception(e)
|
|
return {'error': 'Failed to sign and broadcast preorder transaction'}
|
|
|
|
if 'transaction_hash' in resp:
|
|
if not BLOCKSTACK_DRY_RUN:
|
|
# watch this preorder, and register it when it gets queued
|
|
queue_append("preorder", fqu, resp['transaction_hash'],
|
|
payment_address=payment_address,
|
|
owner_address=owner_address,
|
|
transfer_address=name_data.get('transfer_address'),
|
|
zonefile_data=name_data.get('zonefile'),
|
|
profile=name_data.get('profile'),
|
|
config_path=config_path,
|
|
path=queue_path)
|
|
else:
|
|
assert 'error' in resp
|
|
log.error("Error preordering: %s with %s for %s" % (fqu, payment_address, owner_address))
|
|
log.error("Error below\n%s" % json.dumps(resp, indent=4, sort_keys=True))
|
|
return {'error': 'Failed to preorder: %s' % resp['error']}
|
|
|
|
return resp
|
|
|
|
|
|
def async_register(fqu, payment_privkey_info, owner_privkey_info, tx_fee=None, name_data={},
|
|
proxy=None, config_path=CONFIG_PATH, queue_path=DEFAULT_QUEUE_PATH, safety_checks=True):
|
|
"""
|
|
Register a previously preordered fqu (step #2)
|
|
|
|
@fqu: fully qualified name e.g., muneeb.id
|
|
|
|
Uses from preorder queue:
|
|
@payment_address: used for making the payment
|
|
@owner_address: will own the fqu (must be same as preorder owner_address)
|
|
|
|
Return {'status': True, ...} on success
|
|
Return {'error': ...} on error
|
|
"""
|
|
|
|
if proxy is None:
|
|
proxy = get_default_proxy(config_path=config_path)
|
|
|
|
utxo_client = get_utxo_provider_client(config_path=config_path)
|
|
tx_broadcaster = get_tx_broadcaster( config_path=config_path )
|
|
|
|
owner_address = get_privkey_info_address( owner_privkey_info )
|
|
payment_address = get_privkey_info_address( payment_privkey_info )
|
|
|
|
# check register_queue first
|
|
# stale preorder will get removed from preorder_queue
|
|
if in_queue("register", fqu, path=queue_path):
|
|
log.error("Already in register queue: %s" % fqu)
|
|
return {'error': 'Already in register queue'}
|
|
|
|
if not in_queue("preorder", fqu, path=queue_path):
|
|
log.error("No preorder sent yet: %s" % fqu)
|
|
return {'error': 'No preorder sent yet'}
|
|
|
|
preorder_entry = queue_findone( "preorder", fqu, path=queue_path )
|
|
if len(preorder_entry) == 0:
|
|
log.error("No preorder for '%s'" % fqu)
|
|
return {'error': 'No preorder found'}
|
|
|
|
preorder_tx = preorder_entry[0]['tx_hash']
|
|
tx_confirmations = get_tx_confirmations(preorder_tx, config_path=config_path)
|
|
|
|
if tx_confirmations < PREORDER_CONFIRMATIONS:
|
|
log.error("Waiting on preorder confirmations: (%s, %s)"
|
|
% (preorder_tx, tx_confirmations))
|
|
|
|
return {'error': 'Waiting on preorder confirmations'}
|
|
|
|
try:
|
|
resp = do_register( fqu, payment_privkey_info, owner_privkey_info, utxo_client, tx_broadcaster,
|
|
tx_fee=tx_fee, config_path=config_path, proxy=proxy )
|
|
except Exception, e:
|
|
log.exception(e)
|
|
return {'error': 'Failed to sign and broadcast registration transaction'}
|
|
|
|
if 'transaction_hash' in resp:
|
|
if not BLOCKSTACK_DRY_RUN:
|
|
queue_append("register", fqu, resp['transaction_hash'],
|
|
payment_address=payment_address,
|
|
owner_address=owner_address,
|
|
transfer_address=name_data.get('transfer_address'),
|
|
zonefile_data=name_data.get('zonefile'),
|
|
profile=name_data.get('profile'),
|
|
config_path=config_path,
|
|
path=queue_path)
|
|
|
|
return resp
|
|
|
|
else:
|
|
assert 'error' in resp
|
|
log.error("Error registering: %s" % fqu)
|
|
log.error(resp)
|
|
return {'error': 'Failed to send registration: {}'.format(resp['error'])}
|
|
|
|
|
|
def async_update(fqu, zonefile_data, profile, owner_privkey_info, payment_privkey_info,
|
|
tx_fee=None, name_data={}, config_path=CONFIG_PATH,
|
|
zonefile_hash=None, proxy=None, queue_path=DEFAULT_QUEUE_PATH ):
|
|
"""
|
|
Update a previously registered fqu, using a different payment address
|
|
|
|
@fqu: fully qualified name e.g., muneeb.id
|
|
@zonefile_data: new zonefile text, hash(zonefile) goes to blockchain. If not given, it will be extracted from name_data
|
|
@profile: the name's profile. If not given, it will be extracted from name_data
|
|
@owner_privkey_info: privkey of owner address, to sign update
|
|
@payment_privkey_info: the privkey which is paying for the cost
|
|
|
|
@zonefile_hash: the hash of the zonefile. Must match the zonefile_data (or name_data['zonefile'])
|
|
|
|
return {'status': True} on success
|
|
Return {'error': ...} on error
|
|
"""
|
|
|
|
if zonefile_data is None:
|
|
zonefile_data = name_data.get('zonefile')
|
|
elif name_data.get('zonefile') is not None and zonefile_data != name_data.get('zonefile'):
|
|
assert name_data['zonefile'] == zonefile_data, "Conflicting zone file data given"
|
|
|
|
if profile is None:
|
|
profile = name_data.get('profile')
|
|
elif name_data.get('profile') is not None and profile != name_data.get('profile'):
|
|
assert name_data['profile'] == profile, "Conflicting profile data given"
|
|
|
|
assert zonefile_hash is not None or zonefile_data is not None, "No zone file or zone file hash given"
|
|
|
|
if zonefile_hash is None and zonefile_data is not None:
|
|
zonefile_hash = get_zonefile_data_hash( zonefile_data )
|
|
|
|
if name_data.get('zonefile_hash') is not None:
|
|
assert name_data['zonefile_hash'] == zonefile_hash, "Conflicting zone file hash given"
|
|
|
|
if zonefile_data is not None and len(zonefile_data) > RPC_MAX_ZONEFILE_LEN:
|
|
return {'error': 'Zonefile is too big (%s bytes)' % len(zonefile_data)}
|
|
|
|
if proxy is None:
|
|
proxy = get_default_proxy(config_path=config_path)
|
|
|
|
utxo_client = get_utxo_provider_client(config_path=config_path)
|
|
tx_broadcaster = get_tx_broadcaster(config_path=config_path)
|
|
|
|
owner_address = get_privkey_info_address( owner_privkey_info )
|
|
|
|
if in_queue("update", fqu, path=queue_path):
|
|
log.error("Already in update queue: %s" % fqu)
|
|
return {'error': 'Already in update queue'}
|
|
|
|
resp = {}
|
|
try:
|
|
resp = do_update( fqu, zonefile_hash, owner_privkey_info, payment_privkey_info, utxo_client, tx_broadcaster,
|
|
tx_fee=tx_fee, config_path=config_path, proxy=proxy )
|
|
except Exception, e:
|
|
log.exception(e)
|
|
return {'error': 'Failed to sign and broadcast update transaction'}
|
|
|
|
if 'transaction_hash' in resp:
|
|
if not BLOCKSTACK_DRY_RUN:
|
|
queue_append("update", fqu, resp['transaction_hash'],
|
|
zonefile_data=zonefile_data,
|
|
profile=profile,
|
|
zonefile_hash=zonefile_hash,
|
|
owner_address=owner_address,
|
|
transfer_address=name_data.get('transfer_address'),
|
|
config_path=config_path,
|
|
path=queue_path)
|
|
|
|
resp['zonefile_hash'] = zonefile_hash
|
|
return resp
|
|
|
|
else:
|
|
assert 'error' in resp
|
|
log.error("Error updating: %s" % fqu)
|
|
log.error(resp)
|
|
return {'error': 'Failed to broadcast update transaction: {}'.format(resp['error'])}
|
|
|
|
|
|
def async_transfer(fqu, transfer_address, owner_privkey_info, payment_privkey_info,
|
|
tx_fee=None, config_path=CONFIG_PATH, proxy=None, queue_path=DEFAULT_QUEUE_PATH):
|
|
"""
|
|
Transfer a previously registered fqu, using a different payment address.
|
|
Preserves the zonefile.
|
|
|
|
@fqu: fully qualified name e.g., muneeb.id
|
|
@transfer_address: new owner address
|
|
@owner_privkey_info: privkey of current owner address, to sign tx
|
|
@payment_privkey_info: the key which is paying for the cost
|
|
|
|
Return {'status': True, 'transaction_hash': ...} on success
|
|
Return {'error': ...} on failure
|
|
"""
|
|
|
|
if proxy is None:
|
|
proxy = get_default_proxy(config_path=config_path)
|
|
|
|
utxo_client = get_utxo_provider_client(config_path=config_path)
|
|
tx_broadcaster = get_tx_broadcaster(config_path=config_path)
|
|
|
|
owner_address = get_privkey_info_address( owner_privkey_info )
|
|
|
|
if in_queue("transfer", fqu, path=queue_path):
|
|
log.error("Already in transfer queue: %s" % fqu)
|
|
return {'error': 'Already in transfer queue'}
|
|
|
|
try:
|
|
resp = do_transfer( fqu, transfer_address, True, owner_privkey_info, payment_privkey_info, utxo_client, tx_broadcaster,
|
|
tx_fee=tx_fee, config_path=config_path, proxy=proxy )
|
|
except Exception, e:
|
|
log.exception(e)
|
|
return {'error': 'Failed to sign and broadcast transfer transaction'}
|
|
|
|
if 'transaction_hash' in resp:
|
|
if not BLOCKSTACK_DRY_RUN:
|
|
queue_append("transfer", fqu, resp['transaction_hash'],
|
|
owner_address=owner_address,
|
|
transfer_address=transfer_address,
|
|
config_path=config_path,
|
|
path=queue_path)
|
|
else:
|
|
assert 'error' in resp
|
|
log.error("Error transferring: %s" % fqu)
|
|
log.error(resp)
|
|
return {'error': 'Failed to broadcast transfer transaction: {}'.format(resp['error'])}
|
|
|
|
return resp
|
|
|
|
|
|
def async_renew(fqu, owner_privkey_info, payment_privkey_info, renewal_fee,
|
|
tx_fee=None, proxy=None, config_path=CONFIG_PATH, queue_path=DEFAULT_QUEUE_PATH):
|
|
"""
|
|
Renew an already-registered name.
|
|
|
|
@fqu: fully qualified name e.g., muneeb.id
|
|
|
|
Return {'status': True, ...} on success
|
|
Return {'error': ...} on error
|
|
"""
|
|
|
|
if proxy is None:
|
|
proxy = get_default_proxy(config_path=config_path)
|
|
|
|
owner_address = get_privkey_info_address( owner_privkey_info )
|
|
utxo_client = get_utxo_provider_client(config_path=config_path)
|
|
tx_broadcaster = get_tx_broadcaster( config_path=config_path )
|
|
|
|
# check renew queue first
|
|
if in_queue("renew", fqu, path=queue_path):
|
|
log.error("Already in renew queue: %s" % fqu)
|
|
return {'error': 'Already in renew queue'}
|
|
|
|
try:
|
|
resp = do_renewal( fqu, owner_privkey_info, payment_privkey_info, renewal_fee, utxo_client, tx_broadcaster,
|
|
tx_fee=tx_fee, config_path=config_path, proxy=proxy )
|
|
except Exception, e:
|
|
log.exception(e)
|
|
return {'error': 'Failed to sign and broadcast renewal transaction'}
|
|
|
|
if 'error' in resp or 'transaction_hash' not in resp:
|
|
log.error("Error renewing: %s" % fqu)
|
|
log.error(resp)
|
|
return {'error': 'Failed to send renewal: {}'.format(resp['error'])}
|
|
|
|
else:
|
|
if 'transaction_hash' in resp:
|
|
if not BLOCKSTACK_DRY_RUN:
|
|
queue_append("renew", fqu, resp['transaction_hash'],
|
|
owner_address=owner_address,
|
|
config_path=config_path,
|
|
path=queue_path)
|
|
return resp
|
|
|
|
|
|
def async_revoke(fqu, owner_privkey_info, payment_privkey_info,
|
|
tx_fee=None, proxy=None, config_path=CONFIG_PATH, queue_path=DEFAULT_QUEUE_PATH):
|
|
"""
|
|
Revoke a name.
|
|
|
|
@fqu: fully qualified name e.g., muneeb.id
|
|
|
|
Return {'status': True, ...} on success
|
|
Return {'error': ...} on error
|
|
"""
|
|
|
|
if proxy is None:
|
|
proxy = get_default_proxy(config_path=config_path)
|
|
|
|
owner_address = get_privkey_info_address( owner_privkey_info )
|
|
utxo_client = get_utxo_provider_client(config_path=config_path)
|
|
tx_broadcaster = get_tx_broadcaster( config_path=config_path )
|
|
|
|
# check revoke queue first
|
|
if in_queue("revoke", fqu, path=queue_path):
|
|
log.error("Already in revoke queue: %s" % fqu)
|
|
return {'error': 'Already in revoke queue'}
|
|
|
|
try:
|
|
resp = do_revoke( fqu, owner_privkey_info, payment_privkey_info, utxo_client, tx_broadcaster,
|
|
tx_fee=tx_fee, config_path=config_path, proxy=proxy )
|
|
|
|
except Exception, e:
|
|
log.exception(e)
|
|
return {'error': 'Failed to sign and broadcast revoke transaction'}
|
|
|
|
if 'error' in resp or 'transaction_hash' not in resp:
|
|
log.error("Error revoking: %s" % fqu)
|
|
log.error(resp)
|
|
return {'error': 'Failed to send revoke: {}'.format(resp['error'])}
|
|
|
|
else:
|
|
if 'transaction_hash' in resp:
|
|
if not BLOCKSTACK_DRY_RUN:
|
|
queue_append("revoke", fqu, resp['transaction_hash'],
|
|
owner_address=owner_address,
|
|
config_path=config_path,
|
|
path=queue_path)
|
|
|
|
return resp
|