Skip to content

Commit

Permalink
Introduced support for backward-compatible BIP-329 import and BIP-329…
Browse files Browse the repository at this point in the history
… export.
  • Loading branch information
xavierfiechter committed Sep 18, 2023
1 parent bbfe522 commit 2e835c2
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 12 deletions.
130 changes: 130 additions & 0 deletions electrum/bip329.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import json

class BIP329_Parser:
"""
Source: https://github.com/Labelbase/python-bip329/blob/master/bip329/bip329_parser.py
Version: 0.0.2
"""
def __init__(self, jsonl_path):
self.jsonl_path = jsonl_path
self.entries = []

def load_entries(self):
self.entries = []
with open(self.jsonl_path, 'r') as file:
for line in file:
try:
entry = json.loads(line.strip())
if self.is_valid_entry(entry):
self.entries.append(entry)
except json.JSONDecodeError:
print(f"Skipping invalid JSON line: {line.strip()}")
return self.entries

@staticmethod
def is_valid_entry(entry):
required_keys = {'type', 'ref'}
valid_types = {'tx', 'addr', 'pubkey', 'input', 'output', 'xpub'}

if not required_keys.issubset(entry.keys()):
return False

if 'type' not in entry or entry['type'] not in valid_types:
return False

if entry['type'] == 'output':
if 'spendable' in entry and entry['spendable'] not in {'true', 'false', True, False}:
return False

if 'ref' not in entry:
return False

if 'label' in entry and not isinstance(entry['label'], str):
return False

if 'origin' in entry and not isinstance(entry['origin'], str):
return False

return entry


def is_json_file(path):
""" """
try:
with open(path, 'r', encoding='utf-8') as file:
data = file.read()
# Attempt to parse the content as JSON
json.loads(data)
return True
except (ValueError, FileNotFoundError):
pass
return False


def import_bip329_labels(path, wallet):
"""
Import transaction and address labels, and manage coin (UTXO) state according to BIP-329.
Parameters:
path (str): The file path to the BIP-329 formatted data (JSONP file) to be imported.
wallet: The current wallet.
Behavior:
- The function parses the BIP-329 formatted data located at the specified `path`.
- It loads the entries from the data, including transaction labels, address labels, and coin information.
- For each entry, it performs the following actions based on the entry type:
- If the entry type is "addr" or "tx," it sets labels for transactions and addresses in the wallet.
- If the entry type is "output," it sets labels for specific transactions and determines whether the associated
coins should be spendable or frozen. Coins can be frozen by setting the "spendable" attribute to "false" or
`False`. See also "Coin Management".
Coin Management:
- The function also manages coins (UTXOs) by potentially freezing them based on the provided data.
- Transactions (TXns) are labeled before coin state management.
- Note that this "output" coin management may overwrite a previous "tx" entry if applicable.
- In the context of the Electrum export, TXns are exported before coin state information.
- By default, if no specific information is provided, imported UTXOs are considered spendable (not frozen).
Note:
This function is designed to be used with BIP-329 formatted data and a wallet that supports this standard.
Importing data from other formats *may* not yield the desired results.
Disclaimer:
Ensure that you have a backup of your wallet data before using this function, as it may modify labels and coin
states within your wallet.
"""

parser = BIP329_Parser(path)
entries = parser.load_entries()
for entry in entries:
if entry.get('type', '') in ["addr", "tx"]:
# Set txns and address labels.
wallet.set_label(entry.get('ref', ''), entry.get('label', ''))
elif entry.get('type', '') == "output":
txid, out_idx = entry.get('ref', '').split(":")
wallet.set_label(txid, entry.get('label', ''))
# Set spendable or frozen.
if entry.get("spendable", True) in ["false", False]:
wallet.set_frozen_state_of_coins(utxos=[entry.get('ref', '')], freeze=True)
else:
wallet.set_frozen_state_of_coins(utxos=[entry.get('ref', '')], freeze=False)


def export_bip329_labels(path, wallet):
"""
Transactions (TXns) are exported and labeled before coin state information (spendable).
"""
with open(path, "w") as f:
for key, value in wallet.get_all_labels().items():
data = {
"type": "tx" if len(key) == 64 else "addr",
"ref": key,
"label": value
}
json_line = json.dumps(data, ensure_ascii=False)
f.write(f"{json_line}\n")

for utxo in wallet.get_utxos():
data = {
"type": "output",
"ref": "{}:{}".format(utxo.prevout.txid.hex(), utxo.prevout.out_idx),
"label": wallet.get_label_for_address(utxo.address),
"spendable": "true" if not wallet.is_frozen_coin(utxo) else "false"
}
json_line = json.dumps(data, ensure_ascii=False)
f.write(f"{json_line}\n")
5 changes: 2 additions & 3 deletions electrum/gui/qt/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -2430,10 +2430,10 @@ def do_export_privkeys(self, fileName, pklist, is_csv):
def do_import_labels(self):
def on_import():
self.need_update.set()
import_meta_gui(self, _('labels'), self.wallet.import_labels, on_import)
import_meta_gui(self, _('labels'), self.wallet.import_labels, on_import, file_type="jsonp|json")

def do_export_labels(self):
export_meta_gui(self, _('labels'), self.wallet.export_labels)
export_meta_gui(self, _('labels'), self.wallet.export_labels, file_type="jsonp")

def import_invoices(self):
import_meta_gui(self, _('invoices'), self.wallet.import_invoices, self.send_tab.invoice_list.update)
Expand Down Expand Up @@ -2856,4 +2856,3 @@ def on_swap_result(self, txid):
else:
msg += _("Lightning funds were not received.")
self.show_error_signal.emit(msg)

13 changes: 8 additions & 5 deletions electrum/gui/qt/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -1029,8 +1029,11 @@ def onFileAdded(self, fn):
raise NotImplementedError()


def import_meta_gui(electrum_window: 'ElectrumWindow', title, importer, on_success):
filter_ = "JSON (*.json);;All files (*)"
def import_meta_gui(electrum_window: 'ElectrumWindow', title, importer, on_success, file_type="json"):
if file_type == "json":
filter_ = "JSON (*.json);;All files (*)"
elif file_type == "jsonp|json":
filter_ = "JSONP (*.jsonp);;JSON (*.json);;All files (*)"
filename = getOpenFileName(
parent=electrum_window,
title=_("Open {} file").format(title),
Expand All @@ -1048,12 +1051,12 @@ def import_meta_gui(electrum_window: 'ElectrumWindow', title, importer, on_succe
on_success()


def export_meta_gui(electrum_window: 'ElectrumWindow', title, exporter):
filter_ = "JSON (*.json);;All files (*)"
def export_meta_gui(electrum_window: 'ElectrumWindow', title, exporter, file_type="json"):
filter_ = "{} (*.{});;All files (*)".format(file_type.upper(), file_type)
filename = getSaveFileName(
parent=electrum_window,
title=_("Select file to save your {}").format(title),
filename='electrum_{}.json'.format(title),
filename='electrum_{}.{}'.format(title, file_type),
filter=filter_,
config=electrum_window.config,
)
Expand Down
15 changes: 11 additions & 4 deletions electrum/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@

from .i18n import _
from .bip32 import BIP32Node, convert_bip32_intpath_to_strpath, convert_bip32_strpath_to_intpath
from .bip329 import export_bip329_labels, import_bip329_labels, is_json_file
from .crypto import sha256
from . import util
from .util import (NotEnoughFunds, UserCancelled, profiler, OldTaskGroup, ignore_exceptions,
Expand Down Expand Up @@ -636,12 +637,18 @@ def set_label(self, name: str, text: str = None) -> bool:
return changed

def import_labels(self, path):
data = read_json_file(path)
for key, value in data.items():
self.set_label(key, value)
if is_json_file(path):
# legacy format
data = read_json_file(path)
for key, value in data.items():
self.set_label(key, value)
else:
import_bip329_labels(path=path, wallet=self)

def export_labels(self, path):
write_json_file(path, self.get_all_labels())
""" """
export_bip329_labels(path=path, wallet=self)


def set_fiat_value(self, txid, ccy, text, fx, value_sat):
if not self.db.get_transaction(txid):
Expand Down

0 comments on commit 2e835c2

Please sign in to comment.