Browse Source

Merge pull request #4690 from spesmilo/aiorpcx-fx

asyncio: port exchange_rate and labels to aiohttp
3.3.3.1
ThomasV 6 years ago
committed by GitHub
parent
commit
73bf7a92a2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      contrib/requirements/requirements.txt
  2. 18
      electrum/base_crash_reporter.py
  3. 6
      electrum/daemon.py
  4. 218
      electrum/exchange_rate.py
  5. 2
      electrum/gui/kivy/main_window.py
  6. BIN
      electrum/gui/kivy/theming/light-0.png
  7. 1
      electrum/gui/kivy/theming/light.atlas
  8. 12
      electrum/gui/kivy/uix/dialogs/crash_reporter.py
  9. 11
      electrum/gui/qt/exception_window.py
  10. 15
      electrum/network.py
  11. 84
      electrum/plugins/labels/labels.py
  12. 4
      electrum/plugins/labels/qt.py
  13. 16
      electrum/util.py

2
contrib/requirements/requirements.txt

@ -9,3 +9,5 @@ PySocks>=1.6.6
qdarkstyle<3.0 qdarkstyle<3.0
typing>=3.0.0 typing>=3.0.0
aiorpcx>=0.7.1 aiorpcx>=0.7.1
aiohttp
aiohttp_socks

18
electrum/base_crash_reporter.py

@ -19,6 +19,7 @@
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
import asyncio
import json import json
import locale import locale
import traceback import traceback
@ -26,14 +27,13 @@ import subprocess
import sys import sys
import os import os
import requests
from .version import ELECTRUM_VERSION from .version import ELECTRUM_VERSION
from .import constants from .import constants
from .i18n import _ from .i18n import _
from .util import make_aiohttp_session
class BaseCrashReporter(object): class BaseCrashReporter:
report_server = "https://crashhub.electrum.org" report_server = "https://crashhub.electrum.org"
config_key = "show_crash_reporter" config_key = "show_crash_reporter"
issue_template = """<h2>Traceback</h2> issue_template = """<h2>Traceback</h2>
@ -60,16 +60,22 @@ class BaseCrashReporter(object):
def __init__(self, exctype, value, tb): def __init__(self, exctype, value, tb):
self.exc_args = (exctype, value, tb) self.exc_args = (exctype, value, tb)
def send_report(self, endpoint="/crash"): def send_report(self, asyncio_loop, proxy, endpoint="/crash"):
if constants.net.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in BaseCrashReporter.report_server: if constants.net.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in BaseCrashReporter.report_server:
# Gah! Some kind of altcoin wants to send us crash reports. # Gah! Some kind of altcoin wants to send us crash reports.
raise Exception(_("Missing report URL.")) raise Exception(_("Missing report URL."))
report = self.get_traceback_info() report = self.get_traceback_info()
report.update(self.get_additional_info()) report.update(self.get_additional_info())
report = json.dumps(report) report = json.dumps(report)
response = requests.post(BaseCrashReporter.report_server + endpoint, data=report) coro = self.do_post(proxy, BaseCrashReporter.report_server + endpoint, data=report)
response = asyncio.run_coroutine_threadsafe(coro, asyncio_loop).result(5)
return response return response
async def do_post(self, proxy, url, data):
async with make_aiohttp_session(proxy) as session:
async with session.post(url, data=data) as resp:
return await resp.text()
def get_traceback_info(self): def get_traceback_info(self):
exc_string = str(self.exc_args[1]) exc_string = str(self.exc_args[1])
stack = traceback.extract_tb(self.exc_args[2]) stack = traceback.extract_tb(self.exc_args[2])
@ -125,4 +131,4 @@ class BaseCrashReporter(object):
raise NotImplementedError raise NotImplementedError
def get_os_version(self): def get_os_version(self):
raise NotImplementedError raise NotImplementedError

6
electrum/daemon.py

@ -22,6 +22,7 @@
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
import asyncio
import ast import ast
import os import os
import time import time
@ -126,10 +127,9 @@ class Daemon(DaemonThread):
self.network = None self.network = None
else: else:
self.network = Network(config) self.network = Network(config)
self.network.start()
self.fx = FxThread(config, self.network) self.fx = FxThread(config, self.network)
#if self.network: if self.network:
# self.network.add_jobs([self.fx]) self.network.start(self.fx.run())
self.gui = None self.gui = None
self.wallets = {} self.wallets = {}
# Setup JSONRPC server # Setup JSONRPC server

218
electrum/exchange_rate.py

@ -1,18 +1,21 @@
import asyncio
import aiohttp
from aiohttp_socks import SocksConnector, SocksVer
from datetime import datetime from datetime import datetime
import inspect import inspect
import requests
import sys import sys
import os import os
import json import json
from threading import Thread
import time import time
import csv import csv
import decimal import decimal
from decimal import Decimal from decimal import Decimal
import concurrent.futures
from .bitcoin import COIN from .bitcoin import COIN
from .i18n import _ from .i18n import _
from .util import PrintError, ThreadJob, make_dir from .util import PrintError, ThreadJob, make_dir, aiosafe
from .util import make_aiohttp_session
# See https://en.wikipedia.org/wiki/ISO_4217 # See https://en.wikipedia.org/wiki/ISO_4217
@ -23,6 +26,7 @@ CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0,
'RWF': 0, 'TND': 3, 'UGX': 0, 'UYI': 0, 'VND': 0, 'RWF': 0, 'TND': 3, 'UGX': 0, 'UYI': 0, 'VND': 0,
'VUV': 0, 'XAF': 0, 'XAU': 4, 'XOF': 0, 'XPF': 0} 'VUV': 0, 'XAF': 0, 'XAU': 4, 'XOF': 0, 'XPF': 0}
PROXY = None
class ExchangeBase(PrintError): class ExchangeBase(PrintError):
@ -32,34 +36,41 @@ class ExchangeBase(PrintError):
self.on_quotes = on_quotes self.on_quotes = on_quotes
self.on_history = on_history self.on_history = on_history
def get_json(self, site, get_string): async def get_raw(self, site, get_string):
# APIs must have https # APIs must have https
url = ''.join(['https://', site, get_string]) url = ''.join(['https://', site, get_string])
response = requests.request('GET', url, headers={'User-Agent' : 'Electrum'}, timeout=10) async with make_aiohttp_session(PROXY) as session:
return response.json() async with session.get(url) as response:
return await response.text()
def get_csv(self, site, get_string): async def get_json(self, site, get_string):
# APIs must have https
url = ''.join(['https://', site, get_string]) url = ''.join(['https://', site, get_string])
response = requests.request('GET', url, headers={'User-Agent' : 'Electrum'}) async with make_aiohttp_session(PROXY) as session:
reader = csv.DictReader(response.content.decode().split('\n')) async with session.get(url) as response:
return await response.json()
async def get_csv(self, site, get_string):
raw = await self.get_raw(site, get_string)
reader = csv.DictReader(raw.split('\n'))
return list(reader) return list(reader)
def name(self): def name(self):
return self.__class__.__name__ return self.__class__.__name__
def update_safe(self, ccy): @aiosafe
async def update_safe(self, ccy):
try: try:
self.print_error("getting fx quotes for", ccy) self.print_error("getting fx quotes for", ccy)
self.quotes = self.get_rates(ccy) self.quotes = await self.get_rates(ccy)
self.print_error("received fx quotes") self.print_error("received fx quotes")
except BaseException as e: except BaseException as e:
self.print_error("failed fx quotes:", e) self.print_error("failed fx quotes:", e)
self.quotes = {}
self.on_quotes() self.on_quotes()
def update(self, ccy): def update(self, ccy):
t = Thread(target=self.update_safe, args=(ccy,)) asyncio.get_event_loop().create_task(self.update_safe(ccy))
t.setDaemon(True)
t.start()
def read_historical_rates(self, ccy, cache_dir): def read_historical_rates(self, ccy, cache_dir):
filename = os.path.join(cache_dir, self.name() + '_'+ ccy) filename = os.path.join(cache_dir, self.name() + '_'+ ccy)
@ -78,13 +89,16 @@ class ExchangeBase(PrintError):
self.on_history() self.on_history()
return h return h
def get_historical_rates_safe(self, ccy, cache_dir): @aiosafe
async def get_historical_rates_safe(self, ccy, cache_dir):
try: try:
self.print_error("requesting fx history for", ccy) self.print_error("requesting fx history for", ccy)
h = self.request_history(ccy) h = await self.request_history(ccy)
self.print_error("received fx history for", ccy) self.print_error("received fx history for", ccy)
except BaseException as e: except BaseException as e:
self.print_error("failed fx history:", e) self.print_error("failed fx history:", e)
import traceback
traceback.print_exc()
return return
filename = os.path.join(cache_dir, self.name() + '_' + ccy) filename = os.path.join(cache_dir, self.name() + '_' + ccy)
with open(filename, 'w', encoding='utf-8') as f: with open(filename, 'w', encoding='utf-8') as f:
@ -100,9 +114,7 @@ class ExchangeBase(PrintError):
if h is None: if h is None:
h = self.read_historical_rates(ccy, cache_dir) h = self.read_historical_rates(ccy, cache_dir)
if h is None or h['timestamp'] < time.time() - 24*3600: if h is None or h['timestamp'] < time.time() - 24*3600:
t = Thread(target=self.get_historical_rates_safe, args=(ccy, cache_dir)) asyncio.get_event_loop().create_task(self.get_historical_rates_safe(ccy, cache_dir))
t.setDaemon(True)
t.start()
def history_ccys(self): def history_ccys(self):
return [] return []
@ -116,8 +128,8 @@ class ExchangeBase(PrintError):
class BitcoinAverage(ExchangeBase): class BitcoinAverage(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('apiv2.bitcoinaverage.com', '/indices/global/ticker/short') json = await self.get_json('apiv2.bitcoinaverage.com', '/indices/global/ticker/short')
return dict([(r.replace("BTC", ""), Decimal(json[r]['last'])) return dict([(r.replace("BTC", ""), Decimal(json[r]['last']))
for r in json if r != 'timestamp']) for r in json if r != 'timestamp'])
@ -126,8 +138,8 @@ class BitcoinAverage(ExchangeBase):
'MXN', 'NOK', 'NZD', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'USD', 'MXN', 'NOK', 'NZD', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'USD',
'ZAR'] 'ZAR']
def request_history(self, ccy): async def request_history(self, ccy):
history = self.get_csv('apiv2.bitcoinaverage.com', history = await self.get_csv('apiv2.bitcoinaverage.com',
"/indices/global/history/BTC%s?period=alltime&format=csv" % ccy) "/indices/global/history/BTC%s?period=alltime&format=csv" % ccy)
return dict([(h['DateTime'][:10], h['Average']) return dict([(h['DateTime'][:10], h['Average'])
for h in history]) for h in history])
@ -135,8 +147,8 @@ class BitcoinAverage(ExchangeBase):
class Bitcointoyou(ExchangeBase): class Bitcointoyou(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('bitcointoyou.com', "/API/ticker.aspx") json = await self.get_json('bitcointoyou.com', "/API/ticker.aspx")
return {'BRL': Decimal(json['ticker']['last'])} return {'BRL': Decimal(json['ticker']['last'])}
def history_ccys(self): def history_ccys(self):
@ -145,8 +157,8 @@ class Bitcointoyou(ExchangeBase):
class BitcoinVenezuela(ExchangeBase): class BitcoinVenezuela(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('api.bitcoinvenezuela.com', '/') json = await self.get_json('api.bitcoinvenezuela.com', '/')
rates = [(r, json['BTC'][r]) for r in json['BTC'] rates = [(r, json['BTC'][r]) for r in json['BTC']
if json['BTC'][r] is not None] # Giving NULL for LTC if json['BTC'][r] is not None] # Giving NULL for LTC
return dict(rates) return dict(rates)
@ -154,85 +166,86 @@ class BitcoinVenezuela(ExchangeBase):
def history_ccys(self): def history_ccys(self):
return ['ARS', 'EUR', 'USD', 'VEF'] return ['ARS', 'EUR', 'USD', 'VEF']
def request_history(self, ccy): async def request_history(self, ccy):
return self.get_json('api.bitcoinvenezuela.com', json = await self.get_json('api.bitcoinvenezuela.com',
"/historical/index.php?coin=BTC")[ccy +'_BTC'] "/historical/index.php?coin=BTC")
return json[ccy +'_BTC']
class Bitbank(ExchangeBase): class Bitbank(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('public.bitbank.cc', '/btc_jpy/ticker') json = await self.get_json('public.bitbank.cc', '/btc_jpy/ticker')
return {'JPY': Decimal(json['data']['last'])} return {'JPY': Decimal(json['data']['last'])}
class BitFlyer(ExchangeBase): class BitFlyer(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('bitflyer.jp', '/api/echo/price') json = await self.get_json('bitflyer.jp', '/api/echo/price')
return {'JPY': Decimal(json['mid'])} return {'JPY': Decimal(json['mid'])}
class Bitmarket(ExchangeBase): class Bitmarket(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('www.bitmarket.pl', '/json/BTCPLN/ticker.json') json = await self.get_json('www.bitmarket.pl', '/json/BTCPLN/ticker.json')
return {'PLN': Decimal(json['last'])} return {'PLN': Decimal(json['last'])}
class BitPay(ExchangeBase): class BitPay(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('bitpay.com', '/api/rates') json = await self.get_json('bitpay.com', '/api/rates')
return dict([(r['code'], Decimal(r['rate'])) for r in json]) return dict([(r['code'], Decimal(r['rate'])) for r in json])
class Bitso(ExchangeBase): class Bitso(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('api.bitso.com', '/v2/ticker') json = await self.get_json('api.bitso.com', '/v2/ticker')
return {'MXN': Decimal(json['last'])} return {'MXN': Decimal(json['last'])}
class BitStamp(ExchangeBase): class BitStamp(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('www.bitstamp.net', '/api/ticker/') json = await self.get_json('www.bitstamp.net', '/api/ticker/')
return {'USD': Decimal(json['last'])} return {'USD': Decimal(json['last'])}
class Bitvalor(ExchangeBase): class Bitvalor(ExchangeBase):
def get_rates(self,ccy): async def get_rates(self,ccy):
json = self.get_json('api.bitvalor.com', '/v1/ticker.json') json = await self.get_json('api.bitvalor.com', '/v1/ticker.json')
return {'BRL': Decimal(json['ticker_1h']['total']['last'])} return {'BRL': Decimal(json['ticker_1h']['total']['last'])}
class BlockchainInfo(ExchangeBase): class BlockchainInfo(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('blockchain.info', '/ticker') json = await self.get_json('blockchain.info', '/ticker')
return dict([(r, Decimal(json[r]['15m'])) for r in json]) return dict([(r, Decimal(json[r]['15m'])) for r in json])
class BTCChina(ExchangeBase): class BTCChina(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('data.btcchina.com', '/data/ticker') json = await self.get_json('data.btcchina.com', '/data/ticker')
return {'CNY': Decimal(json['ticker']['last'])} return {'CNY': Decimal(json['ticker']['last'])}
class BTCParalelo(ExchangeBase): class BTCParalelo(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('btcparalelo.com', '/api/price') json = await self.get_json('btcparalelo.com', '/api/price')
return {'VEF': Decimal(json['price'])} return {'VEF': Decimal(json['price'])}
class Coinbase(ExchangeBase): class Coinbase(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('coinbase.com', json = await self.get_json('coinbase.com',
'/api/v1/currencies/exchange_rates') '/api/v1/currencies/exchange_rates')
return dict([(r[7:].upper(), Decimal(json[r])) return dict([(r[7:].upper(), Decimal(json[r]))
for r in json if r.startswith('btc_to_')]) for r in json if r.startswith('btc_to_')])
@ -240,13 +253,13 @@ class Coinbase(ExchangeBase):
class CoinDesk(ExchangeBase): class CoinDesk(ExchangeBase):
def get_currencies(self): async def get_currencies(self):
dicts = self.get_json('api.coindesk.com', dicts = await self.get_json('api.coindesk.com',
'/v1/bpi/supported-currencies.json') '/v1/bpi/supported-currencies.json')
return [d['currency'] for d in dicts] return [d['currency'] for d in dicts]
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('api.coindesk.com', json = await self.get_json('api.coindesk.com',
'/v1/bpi/currentprice/%s.json' % ccy) '/v1/bpi/currentprice/%s.json' % ccy)
result = {ccy: Decimal(json['bpi'][ccy]['rate_float'])} result = {ccy: Decimal(json['bpi'][ccy]['rate_float'])}
return result return result
@ -257,35 +270,35 @@ class CoinDesk(ExchangeBase):
def history_ccys(self): def history_ccys(self):
return self.history_starts().keys() return self.history_starts().keys()
def request_history(self, ccy): async def request_history(self, ccy):
start = self.history_starts()[ccy] start = self.history_starts()[ccy]
end = datetime.today().strftime('%Y-%m-%d') end = datetime.today().strftime('%Y-%m-%d')
# Note ?currency and ?index don't work as documented. Sigh. # Note ?currency and ?index don't work as documented. Sigh.
query = ('/v1/bpi/historical/close.json?start=%s&end=%s' query = ('/v1/bpi/historical/close.json?start=%s&end=%s'
% (start, end)) % (start, end))
json = self.get_json('api.coindesk.com', query) json = await self.get_json('api.coindesk.com', query)
return json['bpi'] return json['bpi']
class Coinsecure(ExchangeBase): class Coinsecure(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('api.coinsecure.in', '/v0/noauth/newticker') json = await self.get_json('api.coinsecure.in', '/v0/noauth/newticker')
return {'INR': Decimal(json['lastprice'] / 100.0 )} return {'INR': Decimal(json['lastprice'] / 100.0 )}
class Foxbit(ExchangeBase): class Foxbit(ExchangeBase):
def get_rates(self,ccy): async def get_rates(self,ccy):
json = self.get_json('api.bitvalor.com', '/v1/ticker.json') json = await self.get_json('api.bitvalor.com', '/v1/ticker.json')
return {'BRL': Decimal(json['ticker_1h']['exchanges']['FOX']['last'])} return {'BRL': Decimal(json['ticker_1h']['exchanges']['FOX']['last'])}
class itBit(ExchangeBase): class itBit(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
ccys = ['USD', 'EUR', 'SGD'] ccys = ['USD', 'EUR', 'SGD']
json = self.get_json('api.itbit.com', '/v1/markets/XBT%s/ticker' % ccy) json = await self.get_json('api.itbit.com', '/v1/markets/XBT%s/ticker' % ccy)
result = dict.fromkeys(ccys) result = dict.fromkeys(ccys)
if ccy in ccys: if ccy in ccys:
result[ccy] = Decimal(json['lastPrice']) result[ccy] = Decimal(json['lastPrice'])
@ -294,10 +307,10 @@ class itBit(ExchangeBase):
class Kraken(ExchangeBase): class Kraken(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
ccys = ['EUR', 'USD', 'CAD', 'GBP', 'JPY'] ccys = ['EUR', 'USD', 'CAD', 'GBP', 'JPY']
pairs = ['XBT%s' % c for c in ccys] pairs = ['XBT%s' % c for c in ccys]
json = self.get_json('api.kraken.com', json = await self.get_json('api.kraken.com',
'/0/public/Ticker?pair=%s' % ','.join(pairs)) '/0/public/Ticker?pair=%s' % ','.join(pairs))
return dict((k[-3:], Decimal(float(v['c'][0]))) return dict((k[-3:], Decimal(float(v['c'][0])))
for k, v in json['result'].items()) for k, v in json['result'].items())
@ -305,45 +318,45 @@ class Kraken(ExchangeBase):
class LocalBitcoins(ExchangeBase): class LocalBitcoins(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('localbitcoins.com', json = await self.get_json('localbitcoins.com',
'/bitcoinaverage/ticker-all-currencies/') '/bitcoinaverage/ticker-all-currencies/')
return dict([(r, Decimal(json[r]['rates']['last'])) for r in json]) return dict([(r, Decimal(json[r]['rates']['last'])) for r in json])
class MercadoBitcoin(ExchangeBase): class MercadoBitcoin(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('api.bitvalor.com', '/v1/ticker.json') json = await self.get_json('api.bitvalor.com', '/v1/ticker.json')
return {'BRL': Decimal(json['ticker_1h']['exchanges']['MBT']['last'])} return {'BRL': Decimal(json['ticker_1h']['exchanges']['MBT']['last'])}
class NegocieCoins(ExchangeBase): class NegocieCoins(ExchangeBase):
def get_rates(self,ccy): async def get_rates(self,ccy):
json = self.get_json('api.bitvalor.com', '/v1/ticker.json') json = await self.get_json('api.bitvalor.com', '/v1/ticker.json')
return {'BRL': Decimal(json['ticker_1h']['exchanges']['NEG']['last'])} return {'BRL': Decimal(json['ticker_1h']['exchanges']['NEG']['last'])}
class TheRockTrading(ExchangeBase): class TheRockTrading(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('api.therocktrading.com', json = await self.get_json('api.therocktrading.com',
'/v1/funds/BTCEUR/ticker') '/v1/funds/BTCEUR/ticker')
return {'EUR': Decimal(json['last'])} return {'EUR': Decimal(json['last'])}
class Unocoin(ExchangeBase): class Unocoin(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('www.unocoin.com', 'trade?buy') json = await self.get_json('www.unocoin.com', 'trade?buy')
return {'INR': Decimal(json)} return {'INR': Decimal(json)}
class WEX(ExchangeBase): class WEX(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json_eur = self.get_json('wex.nz', '/api/3/ticker/btc_eur') json_eur = await self.get_json('wex.nz', '/api/3/ticker/btc_eur')
json_rub = self.get_json('wex.nz', '/api/3/ticker/btc_rur') json_rub = await self.get_json('wex.nz', '/api/3/ticker/btc_rur')
json_usd = self.get_json('wex.nz', '/api/3/ticker/btc_usd') json_usd = await self.get_json('wex.nz', '/api/3/ticker/btc_usd')
return {'EUR': Decimal(json_eur['btc_eur']['last']), return {'EUR': Decimal(json_eur['btc_eur']['last']),
'RUB': Decimal(json_rub['btc_rur']['last']), 'RUB': Decimal(json_rub['btc_rur']['last']),
'USD': Decimal(json_usd['btc_usd']['last'])} 'USD': Decimal(json_usd['btc_usd']['last'])}
@ -351,15 +364,15 @@ class WEX(ExchangeBase):
class Winkdex(ExchangeBase): class Winkdex(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('winkdex.com', '/api/v0/price') json = await self.get_json('winkdex.com', '/api/v0/price')
return {'USD': Decimal(json['price'] / 100.0)} return {'USD': Decimal(json['price'] / 100.0)}
def history_ccys(self): def history_ccys(self):
return ['USD'] return ['USD']
def request_history(self, ccy): async def request_history(self, ccy):
json = self.get_json('winkdex.com', json = await self.get_json('winkdex.com',
"/api/v0/series?start_time=1342915200") "/api/v0/series?start_time=1342915200")
history = json['series'][0]['results'] history = json['series'][0]['results']
return dict([(h['timestamp'][:10], h['price'] / 100.0) return dict([(h['timestamp'][:10], h['price'] / 100.0)
@ -367,8 +380,8 @@ class Winkdex(ExchangeBase):
class Zaif(ExchangeBase): class Zaif(ExchangeBase):
def get_rates(self, ccy): async def get_rates(self, ccy):
json = self.get_json('api.zaif.jp', '/api/1/last_price/btc_jpy') json = await self.get_json('api.zaif.jp', '/api/1/last_price/btc_jpy')
return {'JPY': Decimal(json['last_price'])} return {'JPY': Decimal(json['last_price'])}
@ -381,7 +394,6 @@ def dictinvert(d):
return inv return inv
def get_exchanges_and_currencies(): def get_exchanges_and_currencies():
import os, json
path = os.path.join(os.path.dirname(__file__), 'currencies.json') path = os.path.join(os.path.dirname(__file__), 'currencies.json')
try: try:
with open(path, 'r', encoding='utf-8') as f: with open(path, 'r', encoding='utf-8') as f:
@ -426,13 +438,22 @@ class FxThread(ThreadJob):
def __init__(self, config, network): def __init__(self, config, network):
self.config = config self.config = config
self.network = network self.network = network
self.network.register_callback(self.set_proxy, ['proxy_set'])
self.ccy = self.get_currency() self.ccy = self.get_currency()
self.history_used_spot = False self.history_used_spot = False
self.ccy_combo = None self.ccy_combo = None
self.hist_checkbox = None self.hist_checkbox = None
self.cache_dir = os.path.join(config.path, 'cache') self.cache_dir = os.path.join(config.path, 'cache')
self.trigger = asyncio.Event()
self.trigger.set()
self.set_exchange(self.config_exchange()) self.set_exchange(self.config_exchange())
make_dir(self.cache_dir) make_dir(self.cache_dir)
self.set_proxy('bogus', self.network.proxy)
def set_proxy(self, trigger_name, *args):
global PROXY
PROXY = args[0]
self.trigger.set()
def get_currencies(self, h): def get_currencies(self, h):
d = get_exchanges_by_ccy(h) d = get_exchanges_by_ccy(h)
@ -451,13 +472,18 @@ class FxThread(ThreadJob):
rounded_amount = amount rounded_amount = amount
return fmt_str.format(rounded_amount) return fmt_str.format(rounded_amount)
def run(self): async def run(self):
# This runs from the plugins thread which catches exceptions while True:
if self.is_enabled(): try:
if self.timeout ==0 and self.show_history(): await asyncio.wait_for(self.trigger.wait(), 150)
self.exchange.get_historical_rates(self.ccy, self.cache_dir) except concurrent.futures.TimeoutError:
if self.timeout <= time.time(): pass
self.timeout = time.time() + 150 else:
self.trigger.clear()
if self.is_enabled():
if self.show_history():
self.exchange.get_historical_rates(self.ccy, self.cache_dir)
if self.is_enabled():
self.exchange.update(self.ccy) self.exchange.update(self.ccy)
def is_enabled(self): def is_enabled(self):
@ -497,7 +523,7 @@ class FxThread(ThreadJob):
def set_currency(self, ccy): def set_currency(self, ccy):
self.ccy = ccy self.ccy = ccy
self.config.set_key('currency', ccy, True) self.config.set_key('currency', ccy, True)
self.timeout = 0 # Because self.ccy changes self.trigger.set() # Because self.ccy changes
self.on_quotes() self.on_quotes()
def set_exchange(self, name): def set_exchange(self, name):
@ -508,7 +534,7 @@ class FxThread(ThreadJob):
self.exchange = class_(self.on_quotes, self.on_history) self.exchange = class_(self.on_quotes, self.on_history)
# A new exchange means new fx quotes, initially empty. Force # A new exchange means new fx quotes, initially empty. Force
# a quote refresh # a quote refresh
self.timeout = 0 self.trigger.set()
self.exchange.read_historical_rates(self.ccy, self.cache_dir) self.exchange.read_historical_rates(self.ccy, self.cache_dir)
def on_quotes(self): def on_quotes(self):

2
electrum/gui/kivy/main_window.py

@ -697,7 +697,7 @@ class ElectrumWindow(App):
if not self.wallet: if not self.wallet:
self.status = _("No Wallet") self.status = _("No Wallet")
return return
if self.network is None or not self.network.is_running(): if self.network is None or not self.network.is_connected():
status = _("Offline") status = _("Offline")
elif self.network.is_connected(): elif self.network.is_connected():
server_height = self.network.get_server_height() server_height = self.network.get_server_height()

BIN
electrum/gui/kivy/theming/light-0.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 KiB

1
electrum/gui/kivy/theming/light.atlas

@ -0,0 +1 @@
{"light-0.png": {"electrum_icon640": [2, 702, 320, 320], "nfc_stage_one": [324, 900, 489, 122], "nfc_clock": [2, 460, 243, 240], "stepper_full": [324, 781, 392, 117], "stepper_left": [247, 583, 392, 117], "stepper_restore_password": [247, 464, 392, 117], "stepper_restore_seed": [2, 341, 392, 117], "qrcode": [2, 194, 145, 145], "manualentry": [149, 205, 145, 134], "gear": [2, 33, 105, 159], "calculator": [296, 211, 128, 128], "save": [426, 211, 128, 128], "share": [556, 211, 128, 128], "star_big_inactive": [686, 211, 128, 128], "nfc_phone": [816, 213, 128, 126], "logo": [815, 906, 128, 116], "error": [718, 784, 128, 114], "textinput_active": [848, 784, 114, 114], "close": [641, 612, 88, 88], "important": [731, 612, 88, 88], "paste_icon": [945, 945, 75, 77], "globe": [946, 267, 72, 72], "camera": [821, 636, 64, 64], "carousel_deselected": [887, 636, 64, 64], "carousel_selected": [953, 636, 64, 64], "clock1": [641, 517, 64, 64], "clock2": [707, 517, 64, 64], "clock3": [773, 517, 64, 64], "clock4": [839, 517, 64, 64], "clock5": [905, 517, 64, 64], "confirmed": [324, 715, 64, 64], "contact_overlay": [390, 715, 64, 64], "icon_border": [456, 715, 64, 64], "info": [522, 715, 64, 64], "logo_atom_dull": [588, 715, 64, 64], "nfc": [654, 715, 64, 64], "pen": [720, 715, 64, 64], "shadow": [786, 715, 64, 64], "tab": [852, 715, 64, 64], "unconfirmed": [918, 715, 64, 64], "mail_icon": [396, 404, 65, 54], "settings": [964, 834, 54, 64], "card": [946, 216, 64, 49], "tab_disabled": [641, 483, 96, 32], "tab_strip": [739, 483, 96, 32], "closebutton": [964, 789, 60, 43], "arrow_back": [971, 531, 50, 50], "contact": [463, 409, 49, 49], "wallets": [514, 418, 60, 40], "network": [396, 354, 48, 48], "bit_logo": [109, 141, 44, 51], "add_contact": [446, 359, 51, 43], "wallet": [155, 148, 49, 44], "btn_create_account": [945, 911, 64, 32], "action_group_dark": [984, 731, 33, 48], "action_group_light": [109, 91, 33, 48], "action_bar": [576, 422, 36, 36], "card_btn": [837, 483, 38, 32], "btn_create_act_disabled": [877, 483, 32, 32], "tab_btn": [911, 483, 32, 32], "tab_btn_disabled": [945, 483, 32, 32], "tab_btn_pressed": [979, 483, 32, 32], "dropdown_background": [614, 423, 29, 35], "overflow_background": [645, 423, 29, 35], "blue_bg_round_rb": [821, 614, 31, 20], "lightblue_bg_round_lb": [854, 614, 31, 20], "white_bg_round_top": [887, 614, 31, 20], "card_bottom": [920, 618, 32, 16], "card_top": [954, 618, 32, 16], "dialog": [641, 590, 18, 20], "btn_send_address": [988, 619, 18, 15], "btn_send_nfc": [641, 466, 18, 15], "create_act_text": [984, 719, 22, 10], "create_act_text_active": [971, 519, 22, 10], "action_button_group": [1008, 719, 16, 10], "overflow_btn_dn": [1008, 624, 16, 10], "shadow_right": [641, 583, 32, 5], "btn_nfc": [1011, 931, 13, 12]}}

12
electrum/gui/kivy/uix/dialogs/crash_reporter.py

@ -1,6 +1,7 @@
import sys import sys
import json
import requests from aiohttp.client_exceptions import ClientError
from kivy import base, utils from kivy import base, utils
from kivy.clock import Clock from kivy.clock import Clock
from kivy.core.window import Window from kivy.core.window import Window
@ -102,6 +103,11 @@ class CrashReporter(BaseCrashReporter, Factory.Popup):
self.ids.crash_message.text = BaseCrashReporter.CRASH_MESSAGE self.ids.crash_message.text = BaseCrashReporter.CRASH_MESSAGE
self.ids.request_help_message.text = BaseCrashReporter.REQUEST_HELP_MESSAGE self.ids.request_help_message.text = BaseCrashReporter.REQUEST_HELP_MESSAGE
self.ids.describe_error_message.text = BaseCrashReporter.DESCRIBE_ERROR_MESSAGE self.ids.describe_error_message.text = BaseCrashReporter.DESCRIBE_ERROR_MESSAGE
self.proxy = self.main_window.network.proxy
self.main_window.network.register_callback(self.set_proxy, ['proxy_set'])
def set_proxy(self, evt, proxy):
self.proxy = proxy
def show_contents(self): def show_contents(self):
details = CrashReportDetails(self.get_report_string()) details = CrashReportDetails(self.get_report_string())
@ -115,8 +121,8 @@ class CrashReporter(BaseCrashReporter, Factory.Popup):
def send_report(self): def send_report(self):
try: try:
response = BaseCrashReporter.send_report(self, "/crash.json").json() response = json.loads(BaseCrashReporter.send_report(self, self.main_window.network.asyncio_loop, self.proxy, "/crash.json"))
except requests.exceptions.RequestException: except (ValueError, ClientError):
self.show_popup(_('Unable to send report'), _("Please check your network connection.")) self.show_popup(_('Unable to send report'), _("Please check your network connection."))
else: else:
self.show_popup(_('Report sent'), response["text"]) self.show_popup(_('Report sent'), response["text"])

11
electrum/gui/qt/exception_window.py

@ -41,6 +41,10 @@ class Exception_Window(BaseCrashReporter, QWidget, MessageBoxMixin):
def __init__(self, main_window, exctype, value, tb): def __init__(self, main_window, exctype, value, tb):
BaseCrashReporter.__init__(self, exctype, value, tb) BaseCrashReporter.__init__(self, exctype, value, tb)
self.main_window = main_window self.main_window = main_window
self.proxy = self.main_window.network.proxy
self.main_window.network.register_callback(self.set_proxy, ['proxy_set'])
QWidget.__init__(self) QWidget.__init__(self)
self.setWindowTitle('Electrum - ' + _('An Error Occurred')) self.setWindowTitle('Electrum - ' + _('An Error Occurred'))
self.setMinimumSize(600, 300) self.setMinimumSize(600, 300)
@ -88,16 +92,19 @@ class Exception_Window(BaseCrashReporter, QWidget, MessageBoxMixin):
self.setLayout(main_box) self.setLayout(main_box)
self.show() self.show()
def set_proxy(self, evt, proxy):
self.proxy = proxy
def send_report(self): def send_report(self):
try: try:
response = BaseCrashReporter.send_report(self) response = BaseCrashReporter.send_report(self, self.main_window.network.asyncio_loop, self.proxy)
except BaseException as e: except BaseException as e:
traceback.print_exc(file=sys.stderr) traceback.print_exc(file=sys.stderr)
self.main_window.show_critical(_('There was a problem with the automatic reporting:') + '\n' + self.main_window.show_critical(_('There was a problem with the automatic reporting:') + '\n' +
str(e) + '\n' + str(e) + '\n' +
_("Please report this issue manually.")) _("Please report this issue manually."))
return return
QMessageBox.about(self, _("Crash report"), response.text) QMessageBox.about(self, _("Crash report"), response)
self.close() self.close()
def on_close(self): def on_close(self):

15
electrum/network.py

@ -110,7 +110,7 @@ def pick_random_server(hostmap = None, protocol = 's', exclude_set = set()):
from .simple_config import SimpleConfig from .simple_config import SimpleConfig
proxy_modes = ['socks4', 'socks5', 'http'] proxy_modes = ['socks4', 'socks5']
def serialize_proxy(p): def serialize_proxy(p):
@ -437,6 +437,7 @@ class Network(PrintError):
socket.getaddrinfo = self._fast_getaddrinfo socket.getaddrinfo = self._fast_getaddrinfo
else: else:
socket.getaddrinfo = socket._getaddrinfo socket.getaddrinfo = socket._getaddrinfo
self.trigger_callback('proxy_set', self.proxy)
@staticmethod @staticmethod
def _fast_getaddrinfo(host, *args, **kwargs): def _fast_getaddrinfo(host, *args, **kwargs):
@ -710,9 +711,13 @@ class Network(PrintError):
with b.lock: with b.lock:
b.update_size() b.update_size()
def _run(self): def _run(self, fx):
self.init_headers_file() self.init_headers_file()
self.gat = self.asyncio_loop.create_task(self.maintain_sessions()) jobs = [self.maintain_sessions()]
if fx:
jobs.append(fx)
jobs = [self.asyncio_loop.create_task(x) for x in jobs]
self.gat = asyncio.gather(*jobs)
try: try:
self.asyncio_loop.run_until_complete(self.gat) self.asyncio_loop.run_until_complete(self.gat)
except concurrent.futures.CancelledError: except concurrent.futures.CancelledError:
@ -789,8 +794,8 @@ class Network(PrintError):
def max_checkpoint(cls): def max_checkpoint(cls):
return max(0, len(constants.net.CHECKPOINTS) * 2016 - 1) return max(0, len(constants.net.CHECKPOINTS) * 2016 - 1)
def start(self): def start(self, fx=None):
self.fut = threading.Thread(target=self._run) self.fut = threading.Thread(target=self._run, args=(fx,))
self.fut.start() self.fut.start()
def stop(self): def stop(self):

84
electrum/plugins/labels/labels.py

@ -1,6 +1,6 @@
import asyncio
import hashlib import hashlib
import requests import requests
import threading
import json import json
import sys import sys
import traceback import traceback
@ -10,7 +10,7 @@ import base64
from electrum.plugin import BasePlugin, hook from electrum.plugin import BasePlugin, hook
from electrum.crypto import aes_encrypt_with_iv, aes_decrypt_with_iv from electrum.crypto import aes_encrypt_with_iv, aes_decrypt_with_iv
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import aiosafe, make_aiohttp_session
class LabelsPlugin(BasePlugin): class LabelsPlugin(BasePlugin):
@ -18,11 +18,11 @@ class LabelsPlugin(BasePlugin):
BasePlugin.__init__(self, parent, config, name) BasePlugin.__init__(self, parent, config, name)
self.target_host = 'labels.electrum.org' self.target_host = 'labels.electrum.org'
self.wallets = {} self.wallets = {}
self.proxy = None
def encode(self, wallet, msg): def encode(self, wallet, msg):
password, iv, wallet_id = self.wallets[wallet] password, iv, wallet_id = self.wallets[wallet]
encrypted = aes_encrypt_with_iv(password, iv, encrypted = aes_encrypt_with_iv(password, iv, msg.encode('utf8'))
msg.encode('utf8'))
return base64.b64encode(encrypted).decode() return base64.b64encode(encrypted).decode()
def decode(self, wallet, message): def decode(self, wallet, message):
@ -55,37 +55,27 @@ class LabelsPlugin(BasePlugin):
"walletNonce": nonce, "walletNonce": nonce,
"externalId": self.encode(wallet, item), "externalId": self.encode(wallet, item),
"encryptedLabel": self.encode(wallet, label)} "encryptedLabel": self.encode(wallet, label)}
t = threading.Thread(target=self.do_request_safe, asyncio.get_event_loop().create_task(self.do_post_safe("/label", False, bundle))
args=["POST", "/label", False, bundle])
t.setDaemon(True)
t.start()
# Caller will write the wallet # Caller will write the wallet
self.set_nonce(wallet, nonce + 1) self.set_nonce(wallet, nonce + 1)
def do_request(self, method, url = "/labels", is_batch=False, data=None): @aiosafe
async def do_post_safe(self, *args):
await self.do_post(*args)
async def do_get(self, url = "/labels"):
url = 'https://' + self.target_host + url
async with make_aiohttp_session(self.proxy) as session:
async with session.get(url) as result:
return await result.json()
async def do_post(self, url = "/labels", data=None):
url = 'https://' + self.target_host + url url = 'https://' + self.target_host + url
kwargs = {'headers': {}} async with make_aiohttp_session(self.proxy) as session:
if method == 'GET' and data: async with session.post(url, data=data) as result:
kwargs['params'] = data return await result.json()
elif method == 'POST' and data:
kwargs['data'] = json.dumps(data) async def push_thread(self, wallet):
kwargs['headers']['Content-Type'] = 'application/json'
response = requests.request(method, url, **kwargs)
if response.status_code != 200:
raise Exception(response.status_code, response.text)
response = response.json()
if "error" in response:
raise Exception(response["error"])
return response
def do_request_safe(self, *args, **kwargs):
try:
self.do_request(*args, **kwargs)
except BaseException as e:
#traceback.print_exc(file=sys.stderr)
self.print_error('error doing request')
def push_thread(self, wallet):
wallet_data = self.wallets.get(wallet, None) wallet_data = self.wallets.get(wallet, None)
if not wallet_data: if not wallet_data:
raise Exception('Wallet {} not loaded'.format(wallet)) raise Exception('Wallet {} not loaded'.format(wallet))
@ -102,16 +92,16 @@ class LabelsPlugin(BasePlugin):
continue continue
bundle["labels"].append({'encryptedLabel': encoded_value, bundle["labels"].append({'encryptedLabel': encoded_value,
'externalId': encoded_key}) 'externalId': encoded_key})
self.do_request("POST", "/labels", True, bundle) await self.do_post("/labels", bundle)
def pull_thread(self, wallet, force): async def pull_thread(self, wallet, force):
wallet_data = self.wallets.get(wallet, None) wallet_data = self.wallets.get(wallet, None)
if not wallet_data: if not wallet_data:
raise Exception('Wallet {} not loaded'.format(wallet)) raise Exception('Wallet {} not loaded'.format(wallet))
wallet_id = wallet_data[2] wallet_id = wallet_data[2]
nonce = 1 if force else self.get_nonce(wallet) - 1 nonce = 1 if force else self.get_nonce(wallet) - 1
self.print_error("asking for labels since nonce", nonce) self.print_error("asking for labels since nonce", nonce)
response = self.do_request("GET", ("/labels/since/%d/for/%s" % (nonce, wallet_id) )) response = await self.do_get("/labels/since/%d/for/%s" % (nonce, wallet_id))
if response["labels"] is None: if response["labels"] is None:
self.print_error('no new labels') self.print_error('no new labels')
return return
@ -140,12 +130,15 @@ class LabelsPlugin(BasePlugin):
self.set_nonce(wallet, response["nonce"] + 1) self.set_nonce(wallet, response["nonce"] + 1)
self.on_pulled(wallet) self.on_pulled(wallet)
def pull_thread_safe(self, wallet, force): @aiosafe
try: async def pull_safe_thread(self, wallet, force):
self.pull_thread(wallet, force) await self.pull_thread(wallet, force)
except BaseException as e:
# traceback.print_exc(file=sys.stderr) def pull(self, wallet, force):
self.print_error('could not retrieve labels') return asyncio.run_coroutine_threadsafe(self.pull_thread(wallet, force), wallet.network.asyncio_loop).result()
def push(self, wallet):
return asyncio.run_coroutine_threadsafe(self.push_thread(wallet), wallet.network.asyncio_loop).result()
def start_wallet(self, wallet): def start_wallet(self, wallet):
nonce = self.get_nonce(wallet) nonce = self.get_nonce(wallet)
@ -159,9 +152,14 @@ class LabelsPlugin(BasePlugin):
wallet_id = hashlib.sha256(mpk).hexdigest() wallet_id = hashlib.sha256(mpk).hexdigest()
self.wallets[wallet] = (password, iv, wallet_id) self.wallets[wallet] = (password, iv, wallet_id)
# If there is an auth token we can try to actually start syncing # If there is an auth token we can try to actually start syncing
t = threading.Thread(target=self.pull_thread_safe, args=(wallet, False)) asyncio.get_event_loop().create_task(self.pull_safe_thread(wallet, False))
t.setDaemon(True) self.proxy = wallet.network.proxy
t.start() wallet.network.register_callback(self.set_proxy, ['proxy_set'])
def stop_wallet(self, wallet): def stop_wallet(self, wallet):
wallet.network.unregister_callback('proxy_set')
self.wallets.pop(wallet, None) self.wallets.pop(wallet, None)
def set_proxy(self, evt_name, new_proxy):
self.proxy = new_proxy
self.print_error("proxy set")

4
electrum/plugins/labels/qt.py

@ -38,11 +38,11 @@ class Plugin(LabelsPlugin):
hbox = QHBoxLayout() hbox = QHBoxLayout()
hbox.addWidget(QLabel("Label sync options:")) hbox.addWidget(QLabel("Label sync options:"))
upload = ThreadedButton("Force upload", upload = ThreadedButton("Force upload",
partial(self.push_thread, wallet), partial(self.push, wallet),
partial(self.done_processing_success, d), partial(self.done_processing_success, d),
partial(self.done_processing_error, d)) partial(self.done_processing_error, d))
download = ThreadedButton("Force download", download = ThreadedButton("Force download",
partial(self.pull_thread, wallet, True), partial(self.pull, wallet, True),
partial(self.done_processing_success, d), partial(self.done_processing_success, d),
partial(self.done_processing_error, d)) partial(self.done_processing_error, d))
vbox = QVBoxLayout() vbox = QVBoxLayout()

16
electrum/util.py

@ -38,6 +38,8 @@ import asyncio
from .i18n import _ from .i18n import _
import aiohttp
from aiohttp_socks import SocksConnector, SocksVer
import urllib.request, urllib.parse, urllib.error import urllib.request, urllib.parse, urllib.error
import queue import queue
@ -956,3 +958,17 @@ VerifiedTxInfo = NamedTuple("VerifiedTxInfo", [("height", int),
("timestamp", int), ("timestamp", int),
("txpos", int), ("txpos", int),
("header_hash", str)]) ("header_hash", str)])
def make_aiohttp_session(proxy):
if proxy:
connector = SocksConnector(
socks_ver=SocksVer.SOCKS5 if proxy['mode'] == 'socks5' else SocksVer.SOCKS4,
host=proxy['host'],
port=int(proxy['port']),
username=proxy.get('user', None),
password=proxy.get('password', None),
rdns=True
)
return aiohttp.ClientSession(headers={'User-Agent' : 'Electrum'}, timeout=aiohttp.ClientTimeout(total=10), connector=connector)
else:
return aiohttp.ClientSession(headers={'User-Agent' : 'Electrum'}, timeout=aiohttp.ClientTimeout(total=10))

Loading…
Cancel
Save