electrum

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

commit 1686a97ece31aaa1d2c0e6c7042137a5e8a04943
parent 1b46866e34b7b95ff1c5e0df59b5b6337b4ff68f
Author: SomberNight <somber.night@protonmail.com>
Date:   Mon,  5 Nov 2018 19:31:17 +0100

bip70 PRs: use aiohttp instead of requests. use proxy. small fixes.

Diffstat:
Melectrum/ecc.py | 2+-
Melectrum/gui/qt/main_window.py | 10++++++----
Melectrum/paymentrequest.py | 84++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Melectrum/transaction.py | 3++-
Melectrum/util.py | 13++++++-------
5 files changed, 62 insertions(+), 50 deletions(-)

diff --git a/electrum/ecc.py b/electrum/ecc.py @@ -327,7 +327,7 @@ def verify_message_with_address(address: str, sig65: bytes, message: bytes): public_key.verify_message_hash(sig65[1:], h) return True except Exception as e: - print_error("Verification error: {0}".format(e)) + print_error(f"Verification error: {repr(e)}") return False diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py @@ -36,6 +36,7 @@ from decimal import Decimal import base64 from functools import partial import queue +import asyncio from PyQt5.QtGui import * from PyQt5.QtCore import * @@ -1656,10 +1657,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.invoices.set_paid(pr, tx.txid()) self.invoices.save() self.payment_request = None - refund_address = self.wallet.get_receiving_addresses()[0] - ack_status, ack_msg = pr.send_ack(str(tx), refund_address) - if ack_status: - msg = ack_msg + refund_address = self.wallet.get_receiving_address() + coro = pr.send_payment_and_receive_paymentack(str(tx), refund_address) + fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) + ack_status, ack_msg = fut.result(timeout=20) + msg += f"\n\nPayment ACK: {ack_status}.\nAck message: {ack_msg}" return status, msg # Capture current TL window; override might be removed on return diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py @@ -27,9 +27,10 @@ import sys import time import traceback import json -import requests +import requests import urllib.parse +import aiohttp try: @@ -38,15 +39,17 @@ except ImportError: sys.exit("Error: could not find paymentrequest_pb2.py. Create it with 'protoc --proto_path=electrum/ --python_out=electrum/ electrum/paymentrequest.proto'") from . import bitcoin, ecc, util, transaction, x509, rsakey -from .util import print_error, bh2u, bfh, export_meta, import_meta +from .util import print_error, bh2u, bfh, export_meta, import_meta, make_aiohttp_session from .crypto import sha256 from .bitcoin import TYPE_ADDRESS from .transaction import TxOutput +from .network import Network + REQUEST_HEADERS = {'Accept': 'application/bitcoin-paymentrequest', 'User-Agent': 'Electrum'} ACK_HEADERS = {'Content-Type':'application/bitcoin-payment','Accept':'application/bitcoin-paymentack','User-Agent':'Electrum'} -ca_path = requests.certs.where() +ca_path = requests.certs.where() # FIXME do we need to depend on requests here? ca_list = None ca_keyID = None @@ -64,25 +67,31 @@ PR_UNKNOWN = 2 # sent but not propagated PR_PAID = 3 # send and propagated - -def get_payment_request(url): +async def get_payment_request(url: str) -> 'PaymentRequest': u = urllib.parse.urlparse(url) error = None - if u.scheme in ['http', 'https']: + if u.scheme in ('http', 'https'): + resp_content = None try: - response = requests.request('GET', url, headers=REQUEST_HEADERS) - response.raise_for_status() - # Guard against `bitcoin:`-URIs with invalid payment request URLs - if "Content-Type" not in response.headers \ - or response.headers["Content-Type"] != "application/bitcoin-paymentrequest": - data = None - error = "payment URL not pointing to a payment request handling server" - else: - data = response.content - print_error('fetched payment request', url, len(response.content)) - except requests.exceptions.RequestException: + proxy = Network.get_instance().proxy + async with make_aiohttp_session(proxy, headers=REQUEST_HEADERS) as session: + async with session.get(url) as response: + resp_content = await response.read() + response.raise_for_status() + # Guard against `bitcoin:`-URIs with invalid payment request URLs + if "Content-Type" not in response.headers \ + or response.headers["Content-Type"] != "application/bitcoin-paymentrequest": + data = None + error = "payment URL not pointing to a payment request handling server" + else: + data = resp_content + data_len = len(data) if data is not None else None + print_error('fetched payment request', url, data_len) + except aiohttp.ClientError as e: + error = f"Error while contacting payment URL:\n{repr(e)}" + if isinstance(e, aiohttp.ClientResponseError) and e.status == 400 and resp_content: + error += "\n" + resp_content.decode("utf8") data = None - error = "payment URL not pointing to a valid server" elif u.scheme == 'file': try: with open(u.path, 'r', encoding='utf-8') as f: @@ -92,7 +101,7 @@ def get_payment_request(url): error = "payment URL not pointing to a valid file" else: data = None - error = "Unknown scheme for payment request. URL: {}".format(url) + error = f"Unknown scheme for payment request. URL: {url}" pr = PaymentRequest(data, error) return pr @@ -255,7 +264,7 @@ class PaymentRequest: def get_outputs(self): return self.outputs[:] - def send_ack(self, raw_tx, refund_addr): + async def send_payment_and_receive_paymentack(self, raw_tx, refund_addr): pay_det = self.details if not self.details.payment_url: return False, "no url" @@ -267,24 +276,25 @@ class PaymentRequest: paymnt.memo = "Paid using Electrum" pm = paymnt.SerializeToString() payurl = urllib.parse.urlparse(pay_det.payment_url) + resp_content = None try: - r = requests.post(payurl.geturl(), data=pm, headers=ACK_HEADERS, verify=ca_path) - except requests.exceptions.SSLError: - print("Payment Message/PaymentACK verify Failed") - try: - r = requests.post(payurl.geturl(), data=pm, headers=ACK_HEADERS, verify=False) - except Exception as e: - print(e) - return False, "Payment Message/PaymentACK Failed" - if r.status_code >= 500: - return False, r.reason - try: - paymntack = pb2.PaymentACK() - paymntack.ParseFromString(r.content) - except Exception: - return False, "PaymentACK could not be processed. Payment was sent; please manually verify that payment was received." - print("PaymentACK message received: %s" % paymntack.memo) - return True, paymntack.memo + proxy = Network.get_instance().proxy + async with make_aiohttp_session(proxy, headers=ACK_HEADERS) as session: + async with session.post(payurl.geturl(), data=pm) as response: + resp_content = await response.read() + response.raise_for_status() + try: + paymntack = pb2.PaymentACK() + paymntack.ParseFromString(resp_content) + except Exception: + return False, "PaymentACK could not be processed. Payment was sent; please manually verify that payment was received." + print(f"PaymentACK message received: {paymntack.memo}") + return True, paymntack.memo + except aiohttp.ClientError as e: + error = f"Payment Message/PaymentACK Failed:\n{repr(e)}" + if isinstance(e, aiohttp.ClientResponseError) and e.status == 400 and resp_content: + error += "\n" + resp_content.decode("utf8") + return False, error def make_unsigned_request(req): diff --git a/electrum/transaction.py b/electrum/transaction.py @@ -788,7 +788,8 @@ class Transaction: return self @classmethod - def pay_script(self, output_type, addr): + def pay_script(self, output_type, addr: str) -> str: + """Returns scriptPubKey in hex form.""" if output_type == TYPE_SCRIPT: return addr elif output_type == TYPE_ADDRESS: diff --git a/electrum/util.py b/electrum/util.py @@ -23,7 +23,7 @@ import binascii import os, sys, re, json from collections import defaultdict -from typing import NamedTuple, Union, TYPE_CHECKING, Tuple, Optional +from typing import NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable from datetime import datetime import decimal from decimal import Decimal @@ -693,7 +693,7 @@ def block_explorer_URL(config: 'SimpleConfig', kind: str, item: str) -> Optional #_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) #urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x) -def parse_URI(uri, on_pr=None): +def parse_URI(uri: str, on_pr: Callable=None) -> dict: from . import bitcoin from .bitcoin import COIN @@ -746,18 +746,17 @@ def parse_URI(uri, on_pr=None): sig = out.get('sig') name = out.get('name') if on_pr and (r or (name and sig)): - def get_payment_request_thread(): + async def get_payment_request(): from . import paymentrequest as pr if name and sig: s = pr.serialize_request(out).SerializeToString() request = pr.PaymentRequest(s) else: - request = pr.get_payment_request(r) + request = await pr.get_payment_request(r) if on_pr: on_pr(request) - t = threading.Thread(target=get_payment_request_thread) - t.setDaemon(True) - t.start() + loop = asyncio.get_event_loop() + asyncio.run_coroutine_threadsafe(get_payment_request(), loop) return out