electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 036f96cf35e5de2a6029de0af947f5ca34d821a4
parent 3bac924303b4b4ae2e1009f11544c4afe7b99849
Author: ThomasV <thomasv@gitorious>
Date:   Tue, 11 Nov 2014 11:08:25 +0100

store invoices in a separate file, with their status

Diffstat:
Mgui/qt/main_window.py | 90++++++++++++++++++++++++++++++-------------------------------------------------
Mlib/paymentrequest.py | 151++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mlib/wallet.py | 2+-
Mlib/x509.py | 3+++
4 files changed, 137 insertions(+), 109 deletions(-)

diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py @@ -78,13 +78,8 @@ class StatusBarButton(QPushButton): apply(self.func,()) - -# status of payment requests -PR_UNPAID = 0 -PR_EXPIRED = 1 -PR_SENT = 2 # sent but not propagated -PR_PAID = 3 # send and propagated -PR_ERROR = 4 # could not parse +from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED +from electrum.paymentrequest import PaymentRequest, InvoiceStore, get_payment_request pr_icons = { PR_UNPAID:":icons/unpaid.png", @@ -113,12 +108,13 @@ class ElectrumWindow(QMainWindow): self.lite = None self.app = gui_object.app + self.invoices = InvoiceStore(self.config) + self.create_status_bar() self.need_update = threading.Event() self.decimal_point = config.get('decimal_point', 5) self.num_zeros = int(config.get('num_zeros',0)) - self.invoices = {} self.completions = QStringListModel() @@ -203,7 +199,6 @@ class ElectrumWindow(QMainWindow): a = self.wallet.addresses(False) self.dummy_address = a[0] if a else None - self.invoices = self.wallet.storage.get('invoices', {}) self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{}) self.current_account = self.wallet.storage.get("current_account", None) title = 'Electrum ' + self.wallet.electrum_version + ' - ' + os.path.basename(self.wallet.storage.path) @@ -568,7 +563,8 @@ class ElectrumWindow(QMainWindow): self.receive_address_e = QLineEdit() self.receive_address_e.setReadOnly(True) - grid.addWidget(QLabel(_('Receiving address')), 0, 0) + self.receive_address_label = QLabel(_('Receiving address')) + grid.addWidget(self.receive_address_label, 0, 0) grid.addWidget(self.receive_address_e, 0, 1, 1, 3) self.receive_address_e.textChanged.connect(self.update_receive_qr) @@ -1060,8 +1056,7 @@ class ElectrumWindow(QMainWindow): status, msg = self.wallet.sendtx(tx) if not status: return False, msg - self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash()) - self.wallet.storage.put('invoices', self.invoices) + pr.set_paid(tx.hash()) self.payment_request = None refund_address = self.wallet.addresses()[0] ack_status, ack_msg = pr.send_ack(str(tx), refund_address) @@ -1096,15 +1091,9 @@ class ElectrumWindow(QMainWindow): def payment_request_ok(self): pr = self.payment_request - pr_id = pr.get_id() - if pr_id not in self.invoices: - self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None) - self.wallet.storage.put('invoices', self.invoices) - self.update_invoices_tab() - else: - print_error('invoice already in list') - - status = self.invoices[pr_id][4] + status = pr.get_status() + key = self.invoices.add(pr) + self.update_invoices_tab() if status == PR_PAID: self.do_clear() self.show_message("invoice already paid") @@ -1113,7 +1102,6 @@ class ElectrumWindow(QMainWindow): self.payto_help.show() self.payto_help.set_alt(lambda: self.show_pr_details(pr)) - if not pr.has_expired(): self.payto_e.setGreen() else: @@ -1159,16 +1147,14 @@ class ElectrumWindow(QMainWindow): self.amount_e.textEdited.emit("") return - from electrum import paymentrequest - def payment_request(): - self.payment_request = paymentrequest.PaymentRequest(self.config) - self.payment_request.read(request_url) + def get_payment_request_thread(): + self.payment_request = get_payment_request(request_url) if self.payment_request.verify(): self.emit(SIGNAL('payment_request_ok')) else: self.emit(SIGNAL('payment_request_error')) - self.pr_thread = threading.Thread(target=payment_request).start() + self.pr_thread = threading.Thread(target=get_payment_request_thread).start() self.prepare_for_payment_request() @@ -1227,15 +1213,14 @@ class ElectrumWindow(QMainWindow): return self.create_list_tab(l) def update_invoices_tab(self): - invoices = self.wallet.storage.get('invoices', {}) l = self.invoices_list l.clear() - for key, value in sorted(invoices.items(), key=lambda x: -x[1][3]): - domain, memo, amount, expiration_date, status, tx_hash = value - if status == PR_UNPAID and expiration_date and expiration_date < time.time(): - status = PR_EXPIRED - date_str = format_time(expiration_date) - item = QTreeWidgetItem( [ date_str, domain, memo, self.format_amount(amount, whitespaces=True), ''] ) + for pr in self.invoices.sorted_list(): + key = pr.get_id() + status = pr.get_status() + domain = pr.get_domain() + date_str = format_time(pr.get_expiration_date()) + item = QTreeWidgetItem( [ date_str, domain, pr.memo, self.format_amount(pr.get_amount(), whitespaces=True), ''] ) icon = QIcon(pr_icons.get(status)) item.setIcon(4, icon) item.setToolTip(4, pr_tooltips.get(status,'')) @@ -1385,36 +1370,25 @@ class ElectrumWindow(QMainWindow): run_hook('create_contact_menu', menu, item) menu.exec_(self.contacts_list.viewport().mapToGlobal(position)) - def delete_invoice(self, key): - self.invoices.pop(key) - self.wallet.storage.put('invoices', self.invoices) - self.update_invoices_tab() def show_invoice(self, key): - from electrum.paymentrequest import PaymentRequest - domain, memo, value, expiration, status, tx_hash = self.invoices[key] - pr = PaymentRequest(self.config) - pr.read_file(key) - pr.domain = domain + pr = self.invoices.get(key) pr.verify() - self.show_pr_details(pr, tx_hash) + self.show_pr_details(pr) - def show_pr_details(self, pr, tx_hash=None): - msg = 'Domain: ' + pr.domain - msg += '\nStatus: ' + pr.get_status() + def show_pr_details(self, pr): + msg = 'Requestor: ' + pr.get_domain() + msg += '\nExpires: ' + format_time(pr.get_expiration_date()) + msg += '\nStatus: ' + pr.get_verify_status() msg += '\nMemo: ' + pr.get_memo() msg += '\nPayment URL: ' + pr.payment_url msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[1] + ' ' + self.format_amount(x[2])+ self.base_unit(), pr.get_outputs())) - if tx_hash: - msg += '\n\nTransaction ID: ' + tx_hash + if pr.tx: + msg += '\n\nTransaction ID: ' + pr.tx QMessageBox.information(self, 'Invoice', msg , 'OK') def do_pay_invoice(self, key): - from electrum.paymentrequest import PaymentRequest - domain, memo, value, expiration, status, tx_hash = self.invoices[key] - pr = PaymentRequest(self.config) - pr.read_file(key) - pr.domain = domain + pr = self.invoices.get(key) self.payment_request = pr self.prepare_for_payment_request() if pr.verify(): @@ -1428,12 +1402,16 @@ class ElectrumWindow(QMainWindow): if not item: return key = str(item.data(0, 32).toString()) - domain, memo, value, expiration, status, tx_hash = self.invoices[key] + pr = self.invoices.get(key) + status = pr.get_status() menu = QMenu() menu.addAction(_("Details"), lambda: self.show_invoice(key)) if status == PR_UNPAID: menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key)) - menu.addAction(_("Delete"), lambda: self.delete_invoice(key)) + def delete_invoice(key): + self.wallet.delete_invoice(key) + self.update_invoices_tab() + menu.addAction(_("Delete"), lambda: delete_invoice(key)) menu.exec_(self.invoices_list.viewport().mapToGlobal(position)) diff --git a/lib/paymentrequest.py b/lib/paymentrequest.py @@ -38,7 +38,7 @@ import bitcoin import util import transaction import x509 - +from util import print_error REQUEST_HEADERS = {'Accept': 'application/bitcoin-paymentrequest', 'User-Agent': 'Electrum'} ACK_HEADERS = {'Content-Type':'application/bitcoin-payment','Accept':'application/bitcoin-paymentack','User-Agent':'Electrum'} @@ -47,59 +47,47 @@ ca_path = requests.certs.where() ca_list = x509.load_certificates(ca_path) -class PaymentRequest: - def __init__(self, config): - self.config = config - self.outputs = [] - self.error = "" - self.dir_path = os.path.join( self.config.path, 'requests') - if not os.path.exists(self.dir_path): - os.mkdir(self.dir_path) - - def read(self, url): - self.url = url - u = urlparse.urlparse(url) - self.domain = u.netloc - try: - connection = httplib.HTTPConnection(u.netloc) if u.scheme == 'http' else httplib.HTTPSConnection(u.netloc) - connection.request("GET",u.geturl(), headers=REQUEST_HEADERS) - response = connection.getresponse() - except: - self.error = "cannot read url" - return +# status of payment requests +PR_UNPAID = 0 +PR_EXPIRED = 1 +PR_SENT = 2 # sent but not propagated +PR_PAID = 3 # send and propagated +PR_ERROR = 4 # could not parse - try: - r = response.read() - except: - self.error = "cannot read" - return - - self.id = bitcoin.sha256(r)[0:16].encode('hex') - filename = os.path.join(self.dir_path, self.id) - with open(filename,'wb') as f: - f.write(r) +import json - return self.parse(r) +def get_payment_request(url): + u = urlparse.urlparse(url) + domain = u.netloc + connection = httplib.HTTPConnection(u.netloc) if u.scheme == 'http' else httplib.HTTPSConnection(u.netloc) + connection.request("GET", u.geturl(), headers=REQUEST_HEADERS) + response = connection.getresponse() + data = response.read() + pr = PaymentRequest(data) + return pr - def get_status(self): - if self.error: - return self.error - else: - return self.status +class PaymentRequest: - def read_file(self, key): - filename = os.path.join(self.dir_path, key) - with open(filename,'rb') as f: - r = f.read() + def __init__(self, data): + self.raw = data + self.parse(data) + self.domain = None # known after verify + self.tx = None - assert key == bitcoin.sha256(r)[0:16].encode('hex') - self.id = key - self.parse(r) + def __str__(self): + return self.raw + def get_status(self): + if self.tx is not None: + return PR_PAID + if self.has_expired(): + return PR_EXPIRED + return PR_UNPAID def parse(self, r): + self.id = bitcoin.sha256(r)[0:16].encode('hex') try: self.data = pb2.PaymentRequest() self.data.ParseFromString(r) @@ -108,13 +96,13 @@ class PaymentRequest: return self.details = pb2.PaymentDetails() self.details.ParseFromString(self.data.serialized_payment_details) + self.outputs = [] for o in self.details.outputs: addr = transaction.get_address_from_output_script(o.script)[1] self.outputs.append(('address', addr, o.amount)) self.memo = self.details.memo self.payment_url = self.details.payment_url - def verify(self): if not ca_list: self.error = "Trusted certificate authorities list not found" @@ -135,10 +123,10 @@ class PaymentRequest: if i == 0: try: x.check_date() - x.check_name(self.domain) except Exception as e: self.error = str(e) return + self.domain = x.get_common_name() else: if not x.check_ca(): self.error = "ERROR: Supplied CA Certificate Error" @@ -149,13 +137,10 @@ class PaymentRequest: # if the root CA is not supplied, add it to the chain ca = x509_chain[cert_num-1] supplied_CA_fingerprint = ca.getFingerprint() - supplied_CA_names = ca.extract_names() - CA_OU = supplied_CA_names['OU'] x = ca_list.get(supplied_CA_fingerprint) if x: x.slow_parse() - names = x.extract_names() - assert names['CN'] == supplied_CA_names['CN'] + assert x.get_common_name() == ca.get_common_name() else: issuer = ca.get_issuer() for x in ca_list.values(): @@ -216,7 +201,7 @@ class PaymentRequest: self.error = "ERROR: Invalid Signature for Payment Request Data" return False ### SIG Verified - self.status = 'Signed by Trusted CA:\n' + CA_OU + self.error = 'Signed by Trusted CA: ' + ca.extract_names()['OU'] return True def has_expired(self): @@ -229,7 +214,10 @@ class PaymentRequest: return sum(map(lambda x:x[2], self.outputs)) def get_domain(self): - return self.domain + return self.domain if self.domain else 'unknown' + + def get_verify_status(self): + return self.error def get_memo(self): return self.memo @@ -303,6 +291,65 @@ def make_payment_request(amount, script, memo, rsakey=None): + +class InvoiceStore(object): + + def __init__(self, config): + self.config = config + self.invoices = {} + self.load_invoices() + + def load_invoices(self): + path = os.path.join(self.config.path, 'invoices') + try: + with open(path, 'r') as f: + d = json.loads(f.read()) + except: + return + for k, v in d.items(): + ser, domain, tx = v + try: + pr = PaymentRequest(ser.decode('hex')) + pr.tx = tx + pr.domain = domain + self.invoices[k] = pr + except: + continue + + def save(self): + l = {} + for k, pr in self.invoices.items(): + l[k] = str(pr).encode('hex'), pr.domain, pr.tx + path = os.path.join(self.config.path, 'invoices') + with open(path, 'w') as f: + r = f.write(json.dumps(l)) + + def add(self, pr): + key = pr.get_id() + if key in self.invoices: + print_error('invoice already in list') + return False + self.invoices[key] = pr + self.save() + return key + + def remove(self, key): + self.invoices.pop(key) + self.save() + + def get(self, k): + return self.invoices.get(k) + + def set_paid(self, key, tx_hash): + self.invoices[key].tx = tx_hash + self.save() + + def sorted_list(self): + # sort + return self.invoices.values() + + + if __name__ == "__main__": util.set_verbosity(True) diff --git a/lib/wallet.py b/lib/wallet.py @@ -171,7 +171,6 @@ class Abstract_Wallet(object): self.imported_keys = self.storage.get('imported_keys',{}) self.load_accounts() - self.load_transactions() # spv @@ -221,6 +220,7 @@ class Abstract_Wallet(object): self.history = {} self.save_transactions() + # wizard action def get_action(self): pass diff --git a/lib/x509.py b/lib/x509.py @@ -75,6 +75,9 @@ class X509(tlslite.X509): self.subject = self.tbs.getComponentByName('subject') self.extensions = self.tbs.getComponentByName('extensions') or [] + def get_common_name(self): + return self.extract_names()['CN'] + def get_issuer(self): results = {'CN': None, 'OU': None,} issuer = self.tbs.getComponentByName('issuer')