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 28, 2023
1 parent d7beb10 commit ed2682b
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 12 deletions.
128 changes: 128 additions & 0 deletions electrum/bip329.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import json

class BIP329_Parser:
"""
"""
def __init__(self, json_stream):
self.json_stream = json_stream
self.entries = []

def load_entries(self):
self.entries = []
try:
entries = self.json_stream.strip().split('\n')
for entry in entries:
try:
parsed_entry = json.loads(entry.strip())
if self.is_valid_entry(parsed_entry):
self.entries.append(parsed_entry)
except json.JSONDecodeError:
print(f"Skipping invalid JSON line: {entry.strip()}")
except Exception as e:
print(f"Error processing stream: {e}")
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(stream, wallet):
"""
Import transaction and address labels, and manage coin (UTXO) state according to BIP-329.
Parameters:
stream: The stream object containing the BIP-329 formatted data (JSON Lines) 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(stream)
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(stream, wallet):
"""
Transactions (TXns) are exported and labeled before coin state information (spendable).
"""
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)
stream.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)
stream.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="jsonl|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="jsonl")

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 @@ -1034,8 +1034,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 == "jsonl|json":
filter_ = "JSONL (*.jsonl);;JSON (*.json);;All files (*)"
filename = getOpenFileName(
parent=electrum_window,
title=_("Open {} file").format(title),
Expand All @@ -1053,12 +1056,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
22 changes: 18 additions & 4 deletions electrum/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

import os
import sys
import io
import random
import time
import json
Expand All @@ -51,6 +52,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 +638,24 @@ 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):
data = read_json_file(path) # Process as `legacy format`
for key, value in data.items():
self.set_label(key, value)
else:
with open(path, 'r', encoding='utf-8') as file:
data = file.read()
import_bip329_labels(stream=data, wallet=self)

def export_labels(self, path):
write_json_file(path, self.get_all_labels())
if isinstance(path, str):
output_stream = io.StringIO()
else:
output_stream = path
export_bip329_labels(stream=output_stream, wallet=self)
if isinstance(path, str):
with open(path, 'w', encoding='utf-8') as file:
file.write(output_stream.getvalue())

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

0 comments on commit ed2682b

Please sign in to comment.