diff --git a/electrum_nmc/address_synchronizer.py b/electrum_nmc/address_synchronizer.py index 7e425bfff7c1..72476a4b6e22 100644 --- a/electrum_nmc/address_synchronizer.py +++ b/electrum_nmc/address_synchronizer.py @@ -708,7 +708,7 @@ def get_wallet_delta(self, tx): is_partial = True if not is_mine: is_partial = False - for addr, value in tx.get_outputs(): + for addr, value, name_op in tx.get_outputs(): v_out += value if self.is_mine(addr): v_out_mine += value diff --git a/electrum_nmc/auxpow.py b/electrum_nmc/auxpow.py index 1d491c8eabd3..e1c4c3bfe1fc 100644 --- a/electrum_nmc/auxpow.py +++ b/electrum_nmc/auxpow.py @@ -312,7 +312,7 @@ def fast_txid(tx): def stub_parse_output(vds, i): vds.read_int64() # d['value'] vds.read_bytes(vds.read_compact_size()) # scriptPubKey - return {'type': TYPE_SCRIPT, 'address': None, 'value': 0} + return {'type': TYPE_SCRIPT, 'address': None, 'value': 0, 'name_op': None} # This is equivalent to tx.deserialize(), but doesn't parse outputs. def fast_tx_deserialize(tx): diff --git a/electrum_nmc/commands.py b/electrum_nmc/commands.py index 145e2ee12e52..145717cfdabf 100644 --- a/electrum_nmc/commands.py +++ b/electrum_nmc/commands.py @@ -38,6 +38,7 @@ from . import bitcoin from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS from .i18n import _ +from .names import name_identifier_to_scripthash from .transaction import Transaction, multisig_script, TxOutput from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED from .plugin import run_hook @@ -676,6 +677,81 @@ def getfeerate(self, fee_method=None, fee_level=None): fee_level = Decimal(fee_level) return self.config.fee_per_kb(dyn=dyn, mempool=mempool, fee_level=fee_level) + @command('wn') + def name_show(self, identifier): + # TODO: support non-ASCII encodings + identifier_bytes = identifier.encode("ascii") + sh = name_identifier_to_scripthash(identifier_bytes) + + txs = self.network.run_from_another_thread(self.network.get_history_for_scripthash(sh)) + + # Pick the most recent name op that's [12, 36000) confirmations. + chain_height = self.network.blockchain().height() + safe_height_max = chain_height - 12 + safe_height_min = chain_height - 35999 + + tx_best = None + for tx_candidate in txs[::-1]: + if tx_candidate["height"] <= safe_height_max and tx_candidate["height"] >= safe_height_min: + tx_best = tx_candidate + break + if tx_best is None: + raise Exception("Invalid height") + txid = tx_best["tx_hash"] + height = tx_best["height"] + + # The height is now verified to be safe. + + # TODO: This will write data to the wallet, which may be a privacy + # leak. We should allow a null wallet to be used. + self.network.run_from_another_thread(self.wallet.verifier._request_and_verify_single_proof(txid, height)) + + # The txid is now verified to come from a safe height in the blockchain. + + if self.wallet and txid in self.wallet.transactions: + tx = self.wallet.transactions[txid] + else: + raw = self.network.run_from_another_thread(self.network.get_transaction(txid)) + if raw: + tx = Transaction(raw) + else: + raise Exception("Unknown transaction") + + if tx.txid() != txid: + raise Exception("txid mismatch") + + # the tx is now verified to come from a safe height in the blockchain + + for idx, o in enumerate(tx.outputs()): + if o.name_op is not None: + if "name" in o.name_op: + if o.name_op["name"] != identifier_bytes: + # Identifier mismatch. This will definitely fail under + # current Namecoin consensus rules, but in a future + # hardfork there might be multiple name outputs, so we + # might as well future-proof and scan the other + # outputs. + continue + + # the tx is now verified to represent the identifier at a + # safe height in the blockchain + + return { + "name": o.name_op["name"].decode("ascii"), + "name_encoding": "ascii", + "value": o.name_op["value"].decode("ascii"), + "value_encoding": "ascii", + "txid": txid, + "vout": idx, + "address": o.address, + "height": height, + "expires_in": height - chain_height + 36000, + "expired": False, + "ismine": self.wallet.is_mine(o.address), + } + + raise Exception("missing name op") + @command('') def help(self): # for the python console diff --git a/electrum_nmc/gui/qt/transaction_dialog.py b/electrum_nmc/gui/qt/transaction_dialog.py index 26ebf632eafd..97e54e70ac0a 100644 --- a/electrum_nmc/gui/qt/transaction_dialog.py +++ b/electrum_nmc/gui/qt/transaction_dialog.py @@ -36,6 +36,7 @@ from electrum_nmc.bitcoin import base_encode from electrum_nmc.i18n import _ +from electrum_nmc.names import format_name_op from electrum_nmc.plugin import run_hook from electrum_nmc import simple_config from electrum_nmc.util import bfh @@ -319,11 +320,14 @@ def format_amount(amt): o_text.setFont(QFont(MONOSPACE_FONT)) o_text.setReadOnly(True) cursor = o_text.textCursor() - for addr, v in self.tx.get_outputs(): + for addr, v, name_op in self.tx.get_outputs(): cursor.insertText(addr, text_format(addr)) if v is not None: cursor.insertText('\t', ext) cursor.insertText(format_amount(v), ext) + if name_op is not None: + cursor.insertText('\n', ext) + cursor.insertText(format_name_op(name_op), ext) cursor.insertBlock() vbox.addWidget(o_text) diff --git a/electrum_nmc/names.py b/electrum_nmc/names.py new file mode 100644 index 000000000000..7bd30aaf41d6 --- /dev/null +++ b/electrum_nmc/names.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python +# +# Electrum-NMC - lightweight Namecoin client +# Copyright (C) 2018 Namecoin Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +def split_name_script(decoded): + # This case happens if a script was malformed and couldn't be decoded by + # transaction.get_address_from_output_script. + if decoded is None: + return {"name_op": None, "address_scriptPubKey": decoded} + + # So, Namecoin Core uses OP_0 when pushing an empty string as a (value). + # Unfortunately, Electrum doesn't match OP_0 when using OP_PUSHDATA4 as a + # data push opcode wildcard. So we have to check for OP_0 separately, + # otherwise we'll fail to detect name operations with an empty (value). + # Technically, we should be doing the same check for the (name), but I + # can't be bothered to make the code more complex just to help out whoever + # registered the empty string. The (hash) and (rand) are constant-length + # (at least in practice; not sure about consensus rules), so they're + # unaffected. + + # name_new TxOuts look like: + # NAME_NEW (hash) 2DROP (Bitcoin TxOut) + match = [ OP_NAME_NEW, opcodes.OP_PUSHDATA4, opcodes.OP_2DROP ] + if match_decoded(decoded[:len(match)], match): + return {"name_op": {"op": OP_NAME_NEW, "hash": decoded[1][1]}, "address_scriptPubKey": decoded[len(match):]} + + # name_firstupdate TxOuts look like: + # NAME_FIRSTUPDATE (name) (rand) (value) 2DROP 2DROP (Bitcoin TxOut) + match = [ OP_NAME_FIRSTUPDATE, opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4, opcodes.OP_2DROP, opcodes.OP_2DROP ] + match_empty_value = [ OP_NAME_FIRSTUPDATE, opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4, opcodes.OP_0, opcodes.OP_2DROP, opcodes.OP_2DROP ] + if match_decoded(decoded[:len(match)], match) or match_decoded(decoded[:len(match_empty_value)], match_empty_value): + return {"name_op": {"op": OP_NAME_FIRSTUPDATE, "name": decoded[1][1], "rand": decoded[2][1], "value": decoded[3][1]}, "address_scriptPubKey": decoded[len(match):]} + + # name_update TxOuts look like: + # NAME_UPDATE (name) (value) 2DROP DROP (Bitcoin TxOut) + match = [ OP_NAME_UPDATE, opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4, opcodes.OP_2DROP, opcodes.OP_DROP ] + match_empty_value = [ OP_NAME_UPDATE, opcodes.OP_PUSHDATA4, opcodes.OP_0, opcodes.OP_2DROP, opcodes.OP_DROP ] + if match_decoded(decoded[:len(match)], match) or match_decoded(decoded[:len(match_empty_value)], match_empty_value): + return {"name_op": {"op": OP_NAME_UPDATE, "name": decoded[1][1], "value": decoded[2][1]}, "address_scriptPubKey": decoded[len(match):]} + + return {"name_op": None, "address_scriptPubKey": decoded} + +def get_name_op_from_output_script(_bytes): + try: + decoded = [x for x in script_GetOp(_bytes)] + except MalformedBitcoinScript: + decoded = None + + # Extract the name script if one is present. + return split_name_script(decoded)["name_op"] + +def name_op_to_script(name_op): + if name_op is None: + script = '' + elif name_op["op"] == OP_NAME_NEW: + script = '51' # OP_NAME_NEW + script += push_script(bh2u(name_op["hash"])) + script += '6d' # OP_2DROP + elif name_op["op"] == OP_NAME_FIRSTUPDATE: + script = '52' # OP_NAME_FIRSTUPDATE + script += push_script(bh2u(name_op["name"])) + script += push_script(bh2u(name_op["rand"])) + script += push_script(bh2u(name_op["value"])) + script += '6d' # OP_2DROP + script += '6d' # OP_2DROP + elif name_op["op"] == OP_NAME_UPDATE: + script = '53' # OP_NAME_UPDATE + script += push_script(bh2u(name_op["name"])) + script += push_script(bh2u(name_op["value"])) + script += '6d' # OP_2DROP + script += '75' # OP_DROP + else: + raise BitcoinException('unknown name op: {}'.format(name_op)) + return script + + +def name_identifier_to_scripthash(identifier_bytes): + name_op = {"op": OP_NAME_UPDATE, "name": identifier_bytes, "value": bytes([])} + script = name_op_to_script(name_op) + script += '6a' # OP_RETURN + + return script_to_scripthash(script) + + +def format_name_identifier(identifier_bytes): + try: + identifier = identifier_bytes.decode("ascii") + except UnicodeDecodeError: + return format_name_identifier_unknown_hex(identifier_bytes) + + is_domain_namespace = identifier.startswith("d/") + if is_domain_namespace: + return format_name_identifier_domain(identifier) + + is_identity_namespace = identifier.startswith("id/") + if is_identity_namespace: + return format_name_identifier_identity(identifier) + + return format_name_identifier_unknown(identifier) + + +def format_name_identifier_domain(identifier): + label = identifier[len("d/"):] + + if len(label) < 1: + return format_name_identifier_unknown(identifier) + + # Source: https://github.com/namecoin/proposals/blob/master/ifa-0001.md#keys + if len(label) > 63: + return format_name_identifier_unknown(identifier) + + # Source: https://github.com/namecoin/proposals/blob/master/ifa-0001.md#keys + label_regex = r"^(xn--)?[a-z0-9]+(-[a-z0-9]+)*$" + label_match = re.match(label_regex, label) + if label_match is None: + return format_name_identifier_unknown(identifier) + + # Reject digits-only labels + number_regex = r"^[0-9]+$" + number_match = re.match(number_regex, label) + if number_match is not None: + return format_name_identifier_unknown(identifier) + + return "Domain " + label + ".bit" + + +def format_name_identifier_identity(identifier): + label = identifier[len("id/"):] + + if len(label) < 1: + return format_name_identifier_unknown(identifier) + + # Max id/ identifier length is 255 chars according to wiki spec. But we + # don't need to check for this, because that's also the max length of an + # identifier under the Namecoin consensus rules. + + # Same as d/ regex but without IDN prefix. + # TODO: this doesn't exactly match the https://wiki.namecoin.org spec. + label_regex = r"^[a-z0-9]+(-[a-z0-9]+)*$" + label_match = re.match(label_regex, label) + if label_match is None: + return format_name_identifier_unknown(identifier) + + return "Identity " + label + +def format_name_identifier_unknown(identifier): + # Check for non-printable characters, and print ASCII if none are found. + if identifier.isprintable(): + return 'Non-standard name "' + identifier + '"' + + return format_name_identifier_unknown_hex(identifier.encode("ascii")) + + +def format_name_identifier_unknown_hex(identifier_bytes): + return "Non-standard hex name " + bh2u(identifier_bytes) + + +def format_name_value(identifier_bytes): + try: + identifier = identifier_bytes.decode("ascii") + except UnicodeDecodeError: + return format_name_value_hex(identifier_bytes) + + if not identifier.isprintable(): + return format_name_value_hex(identifier_bytes) + + return "ASCII " + identifier + + +def format_name_value_hex(identifier_bytes): + return "Hex " + bh2u(identifier_bytes) + + +def format_name_op(name_op): + if name_op is None: + return '' + if "hash" in name_op: + formatted_hash = "Commitment = " + bh2u(name_op["hash"]) + if "rand" in name_op: + formatted_rand = "Salt = " + bh2u(name_op["rand"]) + if "name" in name_op: + formatted_name = "Name = " + format_name_identifier(name_op["name"]) + if "value" in name_op: + formatted_value = "Data = " + format_name_value(name_op["value"]) + + if name_op["op"] == OP_NAME_NEW: + return "\tPre-Registration\n\t\t" + formatted_hash + if name_op["op"] == OP_NAME_FIRSTUPDATE: + return "\tRegistration\n\t\t" + formatted_name + "\n\t\t" + formatted_rand + "\n\t\t" + formatted_value + if name_op["op"] == OP_NAME_UPDATE: + return "\tUpdate\n\t\t" + formatted_name + "\n\t\t" + formatted_value + + +def get_default_name_tx_label(wallet, tx): + for addr, v, name_op in tx.get_outputs(): + if name_op is not None: + # TODO: Handle multiple atomic name ops. + name_input_is_mine, name_output_is_mine = get_wallet_name_delta(wallet, tx) + if not name_input_is_mine and not name_output_is_mine: + return None + if name_input_is_mine and not name_output_is_mine: + return "Transfer (Outgoing): " + format_name_identifier(name_op["name"]) + if not name_input_is_mine and name_output_is_mine: + # A name_new transaction isn't expected to have a name input, + # so we don't consider it a transfer. + if name_op["op"] != OP_NAME_NEW: + return "Transfer (Incoming): " + format_name_identifier(name_op["name"]) + if name_op["op"] == OP_NAME_NEW: + # A name_new transaction doesn't have a name output, so there's + # nothing to format. + return "Pre-Registration" + if name_op["op"] == OP_NAME_FIRSTUPDATE: + return "Registration: " + format_name_identifier(name_op["name"]) + if name_op["op"] == OP_NAME_UPDATE: + return "Update: " + format_name_identifier(name_op["name"]) + return None + + +def get_wallet_name_delta(wallet, tx): + name_input_is_mine = False + name_output_is_mine = False + for txin in tx.inputs(): + addr = wallet.get_txin_address(txin) + if wallet.is_mine(addr): + prev_tx = wallet.transactions.get(txin['prevout_hash']) + if prev_tx.get_outputs()[txin['prevout_n']][2] is not None: + name_input_is_mine = True + for addr, value, name_op in tx.get_outputs(): + if name_op is not None and wallet.is_mine(addr): + name_output_is_mine = True + + return name_input_is_mine, name_output_is_mine + + +import binascii +import re + +from .bitcoin import push_script, script_to_scripthash +from .transaction import MalformedBitcoinScript, match_decoded, opcodes, script_GetOp +from .util import bh2u + +OP_NAME_NEW = opcodes.OP_1 +OP_NAME_FIRSTUPDATE = opcodes.OP_2 +OP_NAME_UPDATE = opcodes.OP_3 + diff --git a/electrum_nmc/plugins/hw_wallet/plugin.py b/electrum_nmc/plugins/hw_wallet/plugin.py index 66cd5c55685c..a4816a7f05e4 100644 --- a/electrum_nmc/plugins/hw_wallet/plugin.py +++ b/electrum_nmc/plugins/hw_wallet/plugin.py @@ -116,7 +116,7 @@ def get_library_not_available_message(self) -> str: def is_any_tx_output_on_change_branch(tx): if not hasattr(tx, 'output_info'): return False - for _type, address, amount in tx.outputs(): + for _type, address, amount, name_op in tx.outputs(): info = tx.output_info.get(address) if info is not None: index, xpubs, m = info.address_index, info.sorted_xpubs, info.num_sig diff --git a/electrum_nmc/plugins/ledger/ledger.py b/electrum_nmc/plugins/ledger/ledger.py index e2818645f3d7..450d8229a578 100644 --- a/electrum_nmc/plugins/ledger/ledger.py +++ b/electrum_nmc/plugins/ledger/ledger.py @@ -378,9 +378,9 @@ def sign_transaction(self, tx, password): txOutput = var_int(len(tx.outputs())) for txout in tx.outputs(): - output_type, addr, amount = txout + output_type, addr, amount, name_op = txout txOutput += int_to_hex(amount, 8) - script = tx.pay_script(output_type, addr) + script = tx.pay_script(output_type, addr, name_op) txOutput += var_int(len(script)//2) txOutput += script txOutput = bfh(txOutput) diff --git a/electrum_nmc/tests/test_transaction.py b/electrum_nmc/tests/test_transaction.py index abdf0567854c..b21c3a165c8b 100644 --- a/electrum_nmc/tests/test_transaction.py +++ b/electrum_nmc/tests/test_transaction.py @@ -80,7 +80,8 @@ def test_tx_unsigned(self): 'prevout_n': 0, 'scriptPubKey': '76a914230ac37834073a42146f11ef8414ae929feaafc388ac', 'type': TYPE_ADDRESS, - 'value': 1000000}], + 'value': 1000000, + 'name_op': None}], # name_op=None added by Namecoin 'partial': True, 'segwit_ser': False, 'version': 1, @@ -92,7 +93,8 @@ def test_tx_unsigned(self): self.assertEqual(tx.as_dict(), {'hex': unsigned_blob, 'complete': False, 'final': True}) #self.assertEqual(tx.get_outputs(), [('14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs', 1000000)]) # Converted to Namecoin using `contrib/convertAddress.py` from Namecoin Core. - self.assertEqual(tx.get_outputs(), [('MymekE5Au7Q8MVKJZ19ERsnkRhNanZco6s', 1000000)]) + # name_op=None added by Namecoin + self.assertEqual(tx.get_outputs(), [('MymekE5Au7Q8MVKJZ19ERsnkRhNanZco6s', 1000000, None)]) #self.assertEqual(tx.get_output_addresses(), ['14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs']) # Converted to Namecoin using `contrib/convertAddress.py` from Namecoin Core. self.assertEqual(tx.get_output_addresses(), ['MymekE5Au7Q8MVKJZ19ERsnkRhNanZco6s']) @@ -135,7 +137,8 @@ def test_tx_signed(self): 'prevout_n': 0, 'scriptPubKey': '76a914230ac37834073a42146f11ef8414ae929feaafc388ac', 'type': TYPE_ADDRESS, - 'value': 1000000}], + 'value': 1000000, + 'name_op': None}], # name_op=None added by Namecoin 'partial': False, 'segwit_ser': False, 'version': 1 diff --git a/electrum_nmc/transaction.py b/electrum_nmc/transaction.py index 5f4e7336dafc..9ce18d5e3c6e 100644 --- a/electrum_nmc/transaction.py +++ b/electrum_nmc/transaction.py @@ -63,8 +63,11 @@ class MalformedBitcoinScript(Exception): pass -TxOutput = NamedTuple("TxOutput", [('type', int), ('address', str), ('value', Union[int, str])]) +TxOutput = NamedTuple("TxOutput", [('type', int), ('address', str), ('value', Union[int, str]), ("name_op", dict)]) # ^ value is str when the output is set to max: '!' +TxOutput.__new__.__defaults__ = (None,) +# Assume no name_op if one wasn't provided; this reduces merge conflicts from +# upstream Electrum. Based on https://stackoverflow.com/a/18348004 . TxOutputHwInfo = NamedTuple("TxOutputHwInfo", [('address_index', Tuple), @@ -439,6 +442,9 @@ def get_address_from_output_script(_bytes, *, net=None): except MalformedBitcoinScript: decoded = None + # Strip the name prefix if one is present. + decoded = split_name_script(decoded)["address_scriptPubKey"] + # The Genesis Block, self-payments, and pay-by-IP-address payments look like: # 65 BYTES:... CHECKSIG match = [ opcodes.OP_PUSHDATA4, opcodes.OP_CHECKSIG ] @@ -568,6 +574,12 @@ def parse_output(vds, i): raise SerializationError('invalid output amount (negative)') scriptPubKey = vds.read_bytes(vds.read_compact_size()) d['type'], d['address'] = get_address_from_output_script(scriptPubKey) + d['name_op'] = get_name_op_from_output_script(scriptPubKey) + if d['name_op'] is not None: + # Subtract the 0.01 NMC that's permanently locked in the name + d['value'] = d['value'] - COIN // 100 + if d['value'] < 0: + raise SerializationError('invalid output amount (insufficient for name output)') d['scriptPubKey'] = bh2u(scriptPubKey) d['prevout_n'] = i return d @@ -761,7 +773,7 @@ def deserialize(self, force_full_parse=False): else: d = deserialize(self.raw, force_full_parse, raw_bytes=self.raw_bytes, start_position=self.start_position) self._inputs = d['inputs'] - self._outputs = [TxOutput(x['type'], x['address'], x['value']) for x in d['outputs']] + self._outputs = [TxOutput(x['type'], x['address'], x['value'], x['name_op']) for x in d['outputs']] self.locktime = d['lockTime'] self.version = d['version'] self.is_partial_originally = d['partial'] @@ -793,13 +805,15 @@ def from_io(klass, inputs, outputs, locktime=0): return self @classmethod - def pay_script(self, output_type, addr): + def pay_script(self, output_type, addr, name_op = None): + name_prefix = name_op_to_script(name_op) + if output_type == TYPE_SCRIPT: - return addr + return name_prefix + addr elif output_type == TYPE_ADDRESS: - return bitcoin.address_to_script(addr) + return name_prefix + bitcoin.address_to_script(addr) elif output_type == TYPE_PUBKEY: - return bitcoin.public_key_to_p2pk_script(addr) + return name_prefix + bitcoin.public_key_to_p2pk_script(addr) else: raise TypeError('Unknown output type') @@ -1028,12 +1042,15 @@ def BIP69_sort(self, inputs=True, outputs=True): if inputs: self._inputs.sort(key = lambda i: (i['prevout_hash'], i['prevout_n'])) if outputs: - self._outputs.sort(key = lambda o: (o[2], self.pay_script(o[0], o[1]))) + self._outputs.sort(key = lambda o: (o[2], self.pay_script(o[0], o[1], o[3]))) def serialize_output(self, output): - output_type, addr, amount = output + output_type, addr, amount, name_op = output + if name_op is not None: + # Add the 0.01 NMC that's permanently locked in the name + amount = amount + COIN // 100 s = int_to_hex(amount, 8) - script = self.pay_script(output_type, addr) + script = self.pay_script(output_type, addr, name_op) s += var_int(len(script)//2) s += script return s @@ -1125,7 +1142,7 @@ def input_value(self): return sum(x['value'] for x in self.inputs()) def output_value(self): - return sum(val for tp, addr, val in self.outputs()) + return sum(val for tp, addr, val, name_op in self.outputs()) def get_fee(self): return self.input_value() - self.output_value() @@ -1247,11 +1264,11 @@ def get_outputs(self): addr = bitcoin.public_key_to_p2pkh(bfh(o.address)) else: addr = 'SCRIPT ' + o.address - outputs.append((addr, o.value)) # consider using yield (addr, v) + outputs.append((addr, o.value, o.name_op)) # consider using yield (addr, v) return outputs def get_output_addresses(self): - return [addr for addr, val in self.get_outputs()] + return [addr for addr, val, name_op in self.get_outputs()] def has_address(self, addr): @@ -1285,3 +1302,7 @@ def tx_from_str(txt): tx_dict = json.loads(str(txt)) assert "hex" in tx_dict.keys() return tx_dict["hex"] + + +from .names import get_name_op_from_output_script, name_op_to_script, split_name_script + diff --git a/electrum_nmc/wallet.py b/electrum_nmc/wallet.py index 3d7716e95a1e..745e40f4b131 100644 --- a/electrum_nmc/wallet.py +++ b/electrum_nmc/wallet.py @@ -58,6 +58,8 @@ from .paymentrequest import InvoiceStore from .contacts import Contacts +from .names import get_default_name_tx_label + TX_STATUS = [ _('Unconfirmed'), _('Unconfirmed parent'), @@ -482,6 +484,11 @@ def get_label(self, tx_hash): return label def get_default_label(self, tx_hash): + # TODO: what happens if a name would have a non-empty default non-name label? + name_label = get_default_name_tx_label(self, self.transactions.get(tx_hash)) + if name_label is not None: + return name_label + if self.txi.get(tx_hash) == {}: d = self.txo.get(tx_hash, {}) labels = [] @@ -787,7 +794,7 @@ def add_hw_info(self, tx): info = {} xpubs = self.get_master_public_keys() for txout in tx.outputs(): - _type, addr, amount = txout + _type, addr, amount, name_op = txout if self.is_mine(addr): index = self.get_address_index(addr) pubkeys = self.get_public_keys(addr)