Skip to content

Commit

Permalink
Namecoin: add name_firstupdate and name_update commands
Browse files Browse the repository at this point in the history
  • Loading branch information
JeremyRand committed Oct 3, 2018
1 parent 1ca766d commit d115551
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 20 deletions.
26 changes: 25 additions & 1 deletion electrum_nmc/address_synchronizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,7 +795,7 @@ def get_addr_balance(self, address):
return c, u, x

@with_local_height_cached
def get_utxos(self, domain=None, excluded=None, mature=False, confirmed_only=False, include_names=True):
def get_utxos(self, domain=None, excluded=None, mature=False, confirmed_only=False, include_names=True, only_uno_txids=None, only_uno_identifiers=None):
coins = []
if domain is None:
domain = self.get_addresses()
Expand All @@ -815,6 +815,30 @@ def get_utxos(self, domain=None, excluded=None, mature=False, confirmed_only=Fal
name_op = self.transactions[txid].outputs()[vout].name_op
if name_op is not None:
continue
# The only_uno_txids argument is used to search for name outputs
# from a specific list of txid's, and only return those utxo's.
# In the future it might make more sense to search by
# txid+vout, but for compatibility with Namecoin Core's
# name_firstupdate syntax (where only a txid is specified, not
# a txid+vout) we don't do that right now.
if only_uno_txids is not None:
txid = x['prevout_hash']
vout = x['prevout_n']
name_op = self.transactions[txid].outputs()[vout].name_op
if name_op is None:
continue
if txid not in only_uno_txids:
continue
if only_uno_identifiers is not None:
txid = x['prevout_hash']
vout = x['prevout_n']
name_op = self.transactions[txid].outputs()[vout].name_op
if name_op is None:
continue
if "name" not in name_op:
continue
if name_op["name"] not in only_uno_identifiers:
continue
coins.append(x)
continue
return coins
Expand Down
23 changes: 17 additions & 6 deletions electrum_nmc/coinchooser.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ def change_outputs(self, tx, change_addrs, fee_estimator, dust_threshold):
return change

def make_tx(self, coins, outputs, change_addrs, fee_estimator,
dust_threshold):
dust_threshold, name_coins = []):
"""Select unspent coins to spend to pay outputs. If the change is
greater than dust_threshold (after adding the change output to
the transaction) it is kept, otherwise none is sent and it is
Expand All @@ -196,7 +196,7 @@ def make_tx(self, coins, outputs, change_addrs, fee_estimator,
"""

# Deterministic randomness from coins
utxos = [c['prevout_hash'] + str(c['prevout_n']) for c in coins]
utxos = [c['prevout_hash'] + str(c['prevout_n']) for c in (name_coins + coins)]
self.p = PRNG(''.join(sorted(utxos)))

# Copy the outputs so when adding change we don't modify "outputs"
Expand All @@ -212,23 +212,34 @@ def fee_estimator_w(weight):
return fee_estimator(Transaction.virtual_size_from_weight(weight))

def get_tx_weight(buckets):
total_weight = base_weight + sum(bucket.weight for bucket in buckets)
# Copied from make_Bucket. We're basically constructing the
# witness/weight vars as though the name_coins are their own
# bucket.
name_witness = any(Transaction.is_segwit_input(coin, guess_for_address=True) for coin in name_coins)
# note that we're guessing whether the tx uses segwit based
# on this single bucket
name_weight = sum(Transaction.estimated_input_weight(coin, name_witness)
for coin in name_coins)

total_weight = base_weight + name_weight + sum(bucket.weight for bucket in buckets)
is_segwit_tx = any(bucket.witness for bucket in buckets)
if is_segwit_tx:
total_weight += 2 # marker and flag
# non-segwit inputs were previously assumed to have
# a witness of '' instead of '00' (hex)
# note that mixed legacy/segwit buckets are already ok
num_legacy_name_inputs = (not name_witness) * len(name_coins)
num_legacy_inputs = sum((not bucket.witness) * len(bucket.coins)
for bucket in buckets)
total_weight += num_legacy_inputs
total_weight += num_legacy_name_inputs + num_legacy_inputs

return total_weight

def sufficient_funds(buckets):
'''Given a list of buckets, return True if it has enough
value to pay for the transaction'''
total_input = sum(bucket.value for bucket in buckets)
total_name_input = sum(i["value"] for i in name_coins)
total_input = total_name_input + sum(bucket.value for bucket in buckets)
total_weight = get_tx_weight(buckets)
return total_input >= spent_amount + fee_estimator_w(total_weight)

Expand All @@ -237,7 +248,7 @@ def sufficient_funds(buckets):
buckets = self.choose_buckets(buckets, sufficient_funds,
self.penalty_func(tx))

tx.add_inputs([coin for b in buckets for coin in b.coins])
tx.add_inputs(name_coins + [coin for b in buckets for coin in b.coins])
tx_weight = get_tx_weight(buckets)

# change is sent back to sending address unless specified
Expand Down
55 changes: 50 additions & 5 deletions electrum_nmc/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from . import bitcoin
from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS
from .i18n import _
from .names import build_name_new, name_identifier_to_scripthash
from .names import build_name_new, name_identifier_to_scripthash, OP_NAME_FIRSTUPDATE, OP_NAME_UPDATE
from .transaction import Transaction, multisig_script, TxOutput
from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
from .plugin import run_hook
Expand All @@ -52,6 +52,9 @@ class NameNotFoundError(Exception):
class NameAlreadyExistsError(Exception):
pass

class NamePreRegistrationPendingError(Exception):
pass

def satoshis(amount):
# satoshi conversion must not be performed by the parser
return int(COIN*Decimal(amount)) if amount not in ['!', None] else amount
Expand Down Expand Up @@ -414,7 +417,7 @@ def verifymessage(self, address, signature, message):
message = util.to_bytes(message)
return ecc.verify_message_with_address(address, sig, message)

def _mktx(self, outputs, fee, change_addr, domain, nocheck, unsigned, rbf, password, locktime=None, name_outputs=[]):
def _mktx(self, outputs, fee, change_addr, domain, nocheck, unsigned, rbf, password, locktime=None, name_input_txids=[], name_input_identifiers=[], name_outputs=[]):
self.nocheck = nocheck
change_addr = self._resolver(change_addr)
domain = None if domain is None else map(self._resolver, domain)
Expand All @@ -431,9 +434,10 @@ def _mktx(self, outputs, fee, change_addr, domain, nocheck, unsigned, rbf, passw
amount = satoshis(amount)
final_outputs.append(TxOutput(TYPE_ADDRESS, address, amount))

# TODO: add the name input if the outputs include a name_firstupdate or name_update transaction
coins = self.wallet.get_spendable_coins(domain, self.config)
tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr)
name_coins = self.wallet.get_spendable_coins(domain, self.config, include_names=True, only_uno_txids=name_input_txids)
name_coins += self.wallet.get_spendable_coins(domain, self.config, include_names=True, only_uno_identifiers=name_input_identifiers)
tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr, name_inputs=name_coins)
if locktime != None:
tx.locktime = locktime
if rbf is None:
Expand Down Expand Up @@ -476,12 +480,52 @@ def name_new(self, identifier, destination=None, amount=0.0, fee=None, from_addr
domain = from_addr.split(',') if from_addr else None

# TODO: support non-ASCII encodings
# TODO: enforce length limit on identifier
identifier_bytes = identifier.encode("ascii")
name_op, rand = build_name_new(identifier_bytes)

tx = self._mktx([], tx_fee, change_addr, domain, nocheck, unsigned, rbf, password, locktime, name_outputs=[(destination, amount, name_op)])
return {"tx": tx.as_dict(), "txid": tx.txid(), "rand": bh2u(rand)}

@command('wp')
def name_firstupdate(self, identifier, rand, name_new_txid, value, destination=None, amount=0.0, fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False, rbf=None, password=None, locktime=None, allow_early=False):
"""Create a name_firstupdate transaction. """
if not allow_early:
conf = self.wallet.get_tx_height(name_new_txid).conf
if conf < 12:
remaining_conf = 12 - conf
raise NamePreRegistrationPendingError("The name pre-registration is still pending; wait " + str(remaining_conf) + "more blocks")

tx_fee = satoshis(fee)
domain = from_addr.split(',') if from_addr else None

# TODO: support non-ASCII encodings
# TODO: enforce length limits on identifier and value
# TODO: enforce exact length of rand
identifier_bytes = identifier.encode("ascii")
value_bytes = value.encode("ascii")
rand_bytes = bfh(rand)
name_op = {"op": OP_NAME_FIRSTUPDATE, "name": identifier_bytes, "rand": rand_bytes, "value": value_bytes}

tx = self._mktx([], tx_fee, change_addr, domain, nocheck, unsigned, rbf, password, locktime, name_input_txids=[name_new_txid], name_outputs=[(destination, amount, name_op)])
return tx.as_dict()

@command('wp')
def name_update(self, identifier, value, destination=None, amount=0.0, fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False, rbf=None, password=None, locktime=None):
"""Create a name_update transaction. """

tx_fee = satoshis(fee)
domain = from_addr.split(',') if from_addr else None

# TODO: support non-ASCII encodings
# TODO: enforce length limits on identifier and value
identifier_bytes = identifier.encode("ascii")
value_bytes = value.encode("ascii")
name_op = {"op": OP_NAME_UPDATE, "name": identifier_bytes, "value": value_bytes}

tx = self._mktx([], tx_fee, change_addr, domain, nocheck, unsigned, rbf, password, locktime, name_input_identifiers=[identifier_bytes], name_outputs=[(destination, amount, name_op)])
return tx.as_dict()

@command('w')
def history(self, year=None, show_addresses=False, show_fiat=False):
"""Wallet history. Returns the transaction history of your wallet."""
Expand Down Expand Up @@ -849,7 +893,8 @@ def help(self):
'fee_level': (None, "Float between 0.0 and 1.0, representing fee slider position"),
'destination': (None, "Namecoin address, contact or alias"),
'amount': (None, "Amount to be sent (in NMC). Type \'!\' to send the maximum available."),
'allow_existing': (None, "Allow pre-registering a name that already is registered"),
'allow_existing': (None, "Allow pre-registering a name that already is registered. Your registration fee will be forfeited until you can register the name after it expires."),
'allow_early': (None, "Allow submitting a name registration while its pre-registration is still pending. This increases the risk of an attacker stealing your name registration."),
}


Expand Down
13 changes: 9 additions & 4 deletions electrum_nmc/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -1003,19 +1003,24 @@ def get_preimage_script(self, txin):

pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin)
if txin['type'] == 'p2pkh':
return bitcoin.address_to_script(txin['address'])
result = bitcoin.address_to_script(txin['address'])
elif txin['type'] in ['p2sh', 'p2wsh', 'p2wsh-p2sh']:
return multisig_script(pubkeys, txin['num_sig'])
result = multisig_script(pubkeys, txin['num_sig'])
elif txin['type'] in ['p2wpkh', 'p2wpkh-p2sh']:
pubkey = pubkeys[0]
pkh = bh2u(bitcoin.hash_160(bfh(pubkey)))
return '76a9' + push_script(pkh) + '88ac'
result = '76a9' + push_script(pkh) + '88ac'
elif txin['type'] == 'p2pk':
pubkey = pubkeys[0]
return bitcoin.public_key_to_p2pk_script(pubkey)
result = bitcoin.public_key_to_p2pk_script(pubkey)
else:
raise TypeError('Unknown txin type', txin['type'])

if 'name_op' in txin and txin['name_op'] is not None:
result = name_op_to_script(txin['name_op']) + result

return result

@classmethod
def serialize_outpoint(self, txin):
return bh2u(bfh(txin['prevout_hash'])[::-1]) + int_to_hex(txin['prevout_n'], 4)
Expand Down
18 changes: 14 additions & 4 deletions electrum_nmc/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,9 +366,9 @@ def get_tx_info(self, tx):

return tx_hash, status, label, can_broadcast, can_bump, amount, fee, height, conf, timestamp, exp_n

def get_spendable_coins(self, domain, config, include_names=False):
def get_spendable_coins(self, domain, config, include_names=False, only_uno_txids=None, only_uno_identifiers=None):
confirmed_only = config.get('confirmed_only', False)
return self.get_utxos(domain, excluded=self.frozen_addresses, mature=True, confirmed_only=confirmed_only, include_names=include_names)
return self.get_utxos(domain, excluded=self.frozen_addresses, mature=True, confirmed_only=confirmed_only, include_names=include_names, only_uno_txids=only_uno_txids, only_uno_identifiers=only_uno_identifiers)

def dummy_address(self):
return self.get_receiving_addresses()[0]
Expand Down Expand Up @@ -546,7 +546,7 @@ def dust_threshold(self):
return dust_threshold(self.network)

def make_unsigned_transaction(self, inputs, outputs, config, fixed_fee=None,
change_addr=None, is_sweep=False):
change_addr=None, is_sweep=False, name_inputs=[]):
# check outputs
i_max = None
for i, o in enumerate(outputs):
Expand All @@ -568,6 +568,9 @@ def make_unsigned_transaction(self, inputs, outputs, config, fixed_fee=None,
for item in inputs:
self.add_input_info(item)

for item in name_inputs:
self.add_input_info(item)

# change address
# if we leave it empty, coin_chooser will set it
change_addrs = []
Expand Down Expand Up @@ -601,8 +604,9 @@ def make_unsigned_transaction(self, inputs, outputs, config, fixed_fee=None,
max_change = self.max_change_outputs if self.multiple_change else 1
coin_chooser = coinchooser.get_coin_chooser(config)
tx = coin_chooser.make_tx(inputs, outputs, change_addrs[:max_change],
fee_estimator, self.dust_threshold())
fee_estimator, self.dust_threshold(), name_coins=name_inputs)
else:
inputs = name_inputs + inputs
# FIXME?? this might spend inputs with negative effective value...
sendable = sum(map(lambda x:x['value'], inputs))
outputs[i_max] = outputs[i_max]._replace(value=0)
Expand Down Expand Up @@ -752,6 +756,12 @@ def add_input_info(self, txin):
tx_height, value, is_cb = item
txin['value'] = value
self.add_input_sig_info(txin, address)
if 'name_op' not in txin:
if txin['prevout_hash'] in self.transactions:
prevouts = self.transactions[txin['prevout_hash']].outputs()
if txin['prevout_n'] < len(prevouts):
prevout = prevouts[txin['prevout_n']]
txin['name_op'] = prevout.name_op

def add_input_info_to_all_inputs(self, tx):
if tx.is_complete():
Expand Down

0 comments on commit d115551

Please sign in to comment.