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:
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