2018-05-25 08:03:25 +02:00
|
|
|
import logging
|
2018-10-19 16:45:05 +02:00
|
|
|
import asyncio
|
2018-10-17 19:32:45 +02:00
|
|
|
from asyncio import CancelledError
|
2018-12-11 05:38:15 +01:00
|
|
|
from time import time
|
2018-10-15 04:16:51 +02:00
|
|
|
|
2018-12-05 07:01:11 +01:00
|
|
|
from torba.rpc import RPCSession as BaseClientSession, Connector, RPCError
|
2018-05-25 08:03:25 +02:00
|
|
|
|
|
|
|
from torba import __version__
|
|
|
|
from torba.stream import StreamController
|
|
|
|
|
2018-07-01 23:20:17 +02:00
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
2018-05-25 08:03:25 +02:00
|
|
|
|
2018-10-15 04:16:51 +02:00
|
|
|
class ClientSession(BaseClientSession):
|
2018-05-25 08:03:25 +02:00
|
|
|
|
2018-10-30 21:26:07 +01:00
|
|
|
def __init__(self, *args, network, server, **kwargs):
|
2018-05-25 08:03:25 +02:00
|
|
|
self.network = network
|
2018-10-30 21:26:07 +01:00
|
|
|
self.server = server
|
2018-10-15 04:16:51 +02:00
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self._on_disconnect_controller = StreamController()
|
|
|
|
self.on_disconnected = self._on_disconnect_controller.stream
|
2018-10-17 19:05:47 +02:00
|
|
|
self.bw_limit = self.framer.max_size = self.max_errors = 1 << 32
|
2018-12-11 05:38:15 +01:00
|
|
|
self.max_seconds_idle = 60
|
|
|
|
self.ping_task = None
|
2018-10-15 04:16:51 +02:00
|
|
|
|
2018-11-25 22:30:55 +01:00
|
|
|
async def send_request(self, method, args=()):
|
|
|
|
try:
|
|
|
|
return await super().send_request(method, args)
|
|
|
|
except RPCError as e:
|
|
|
|
log.warning("Wallet server returned an error. Code: %s Message: %s", *e.args)
|
|
|
|
raise e
|
|
|
|
|
2018-12-11 05:38:15 +01:00
|
|
|
async def ping_forever(self):
|
|
|
|
# TODO: change to 'ping' on newer protocol (above 1.2)
|
|
|
|
while not self.is_closing():
|
|
|
|
if (time() - self.last_send) > self.max_seconds_idle:
|
|
|
|
await self.send_request('server.banner')
|
|
|
|
await asyncio.sleep(self.max_seconds_idle//3)
|
|
|
|
|
2019-02-12 22:51:12 +01:00
|
|
|
async def create_connection(self, timeout=6):
|
2018-10-30 21:26:07 +01:00
|
|
|
connector = Connector(lambda: self, *self.server)
|
2019-02-12 22:51:12 +01:00
|
|
|
await asyncio.wait_for(connector.create_connection(), timeout=timeout)
|
2018-12-11 05:38:15 +01:00
|
|
|
self.ping_task = asyncio.create_task(self.ping_forever())
|
2018-10-30 21:26:07 +01:00
|
|
|
|
2018-10-15 04:16:51 +02:00
|
|
|
async def handle_request(self, request):
|
|
|
|
controller = self.network.subscription_controllers[request.method]
|
|
|
|
controller.add(request.args)
|
2018-05-25 08:03:25 +02:00
|
|
|
|
2018-10-15 04:16:51 +02:00
|
|
|
def connection_lost(self, exc):
|
|
|
|
super().connection_lost(exc)
|
|
|
|
self._on_disconnect_controller.add(True)
|
2018-12-11 05:38:15 +01:00
|
|
|
if self.ping_task:
|
|
|
|
self.ping_task.cancel()
|
2018-05-25 08:03:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
class BaseNetwork:
|
|
|
|
|
2018-06-08 05:47:46 +02:00
|
|
|
def __init__(self, ledger):
|
|
|
|
self.config = ledger.config
|
2018-10-15 04:16:51 +02:00
|
|
|
self.client: ClientSession = None
|
2018-05-25 08:03:25 +02:00
|
|
|
self.running = False
|
|
|
|
|
|
|
|
self._on_connected_controller = StreamController()
|
|
|
|
self.on_connected = self._on_connected_controller.stream
|
|
|
|
|
|
|
|
self._on_header_controller = StreamController()
|
|
|
|
self.on_header = self._on_header_controller.stream
|
|
|
|
|
|
|
|
self._on_status_controller = StreamController()
|
|
|
|
self.on_status = self._on_status_controller.stream
|
|
|
|
|
|
|
|
self.subscription_controllers = {
|
2018-07-15 03:34:07 +02:00
|
|
|
'blockchain.headers.subscribe': self._on_header_controller,
|
|
|
|
'blockchain.address.subscribe': self._on_status_controller,
|
2018-05-25 08:03:25 +02:00
|
|
|
}
|
|
|
|
|
2019-06-03 10:21:57 +02:00
|
|
|
async def pick_fastest_server(self, timeout):
|
|
|
|
async def __probe(server):
|
|
|
|
client = ClientSession(network=self, server=server)
|
|
|
|
try:
|
|
|
|
await client.create_connection(timeout)
|
|
|
|
await client.send_request('server.banner')
|
|
|
|
return client
|
|
|
|
except (asyncio.TimeoutError, asyncio.CancelledError) as error:
|
|
|
|
client.connection_lost(error)
|
|
|
|
raise error
|
|
|
|
futures = []
|
|
|
|
for server in self.config['default_servers']:
|
|
|
|
futures.append(__probe(server))
|
|
|
|
done, pending = await asyncio.wait(futures, return_when='FIRST_COMPLETED')
|
|
|
|
for task in pending:
|
|
|
|
task.cancel()
|
|
|
|
for client in done:
|
|
|
|
return await client
|
|
|
|
|
2018-10-15 04:16:51 +02:00
|
|
|
async def start(self):
|
2018-09-17 20:32:16 +02:00
|
|
|
self.running = True
|
2018-10-19 16:45:05 +02:00
|
|
|
delay = 0.0
|
2019-02-12 22:51:12 +01:00
|
|
|
connect_timeout = self.config.get('connect_timeout', 6)
|
2019-06-03 10:21:57 +02:00
|
|
|
while True:
|
|
|
|
self.client = await self.pick_fastest_server(connect_timeout)
|
|
|
|
connection_string = '{}:{}'.format(*self.client.server)
|
2018-05-25 08:03:25 +02:00
|
|
|
try:
|
2018-10-15 04:16:51 +02:00
|
|
|
await self.ensure_server_version()
|
2018-10-16 17:56:53 +02:00
|
|
|
log.info("Successfully connected to SPV wallet server: %s", connection_string)
|
2018-05-25 08:03:25 +02:00
|
|
|
self._on_connected_controller.add(True)
|
2018-10-19 16:45:05 +02:00
|
|
|
delay = 0.0
|
2018-10-15 04:16:51 +02:00
|
|
|
await self.client.on_disconnected.first
|
2019-01-09 17:29:28 +01:00
|
|
|
except CancelledError:
|
|
|
|
self.running = False
|
2019-02-12 22:51:12 +01:00
|
|
|
except asyncio.TimeoutError:
|
|
|
|
log.warning("Timed out connecting to: %s", connection_string)
|
2019-01-09 17:29:28 +01:00
|
|
|
except Exception: # pylint: disable=broad-except
|
2018-07-29 02:52:54 +02:00
|
|
|
log.exception("Connecting to %s raised an exception:", connection_string)
|
2018-05-25 08:03:25 +02:00
|
|
|
if not self.running:
|
|
|
|
return
|
2018-10-17 19:32:33 +02:00
|
|
|
elif self.client:
|
|
|
|
await self.client.close()
|
|
|
|
self.client.connection.cancel_pending_requests()
|
2018-10-19 16:45:05 +02:00
|
|
|
await asyncio.sleep(delay)
|
|
|
|
delay = min(delay + 1.0, 10.0)
|
2018-05-25 08:03:25 +02:00
|
|
|
|
2018-10-15 04:16:51 +02:00
|
|
|
async def stop(self):
|
2018-05-25 08:03:25 +02:00
|
|
|
self.running = False
|
|
|
|
if self.is_connected:
|
2018-10-15 06:45:21 +02:00
|
|
|
disconnected = self.client.on_disconnected.first
|
2018-10-15 04:16:51 +02:00
|
|
|
await self.client.close()
|
2018-10-15 06:45:21 +02:00
|
|
|
await disconnected
|
2018-05-25 08:03:25 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def is_connected(self):
|
2018-10-15 04:16:51 +02:00
|
|
|
return self.client is not None and not self.client.is_closing()
|
2018-05-25 08:03:25 +02:00
|
|
|
|
2019-04-07 00:08:33 +02:00
|
|
|
def rpc(self, list_or_method, args):
|
2018-05-25 08:03:25 +02:00
|
|
|
if self.is_connected:
|
2018-10-15 04:16:51 +02:00
|
|
|
return self.client.send_request(list_or_method, args)
|
2018-05-25 08:03:25 +02:00
|
|
|
else:
|
|
|
|
raise ConnectionError("Attempting to send rpc request when connection is not available.")
|
|
|
|
|
|
|
|
def ensure_server_version(self, required='1.2'):
|
2019-04-07 00:08:33 +02:00
|
|
|
return self.rpc('server.version', [__version__, required])
|
2018-05-25 08:03:25 +02:00
|
|
|
|
|
|
|
def broadcast(self, raw_transaction):
|
2019-04-07 00:08:33 +02:00
|
|
|
return self.rpc('blockchain.transaction.broadcast', [raw_transaction])
|
2018-05-25 08:03:25 +02:00
|
|
|
|
|
|
|
def get_history(self, address):
|
2019-04-07 00:08:33 +02:00
|
|
|
return self.rpc('blockchain.address.get_history', [address])
|
2018-05-25 08:03:25 +02:00
|
|
|
|
|
|
|
def get_transaction(self, tx_hash):
|
2019-04-07 00:08:33 +02:00
|
|
|
return self.rpc('blockchain.transaction.get', [tx_hash])
|
2018-05-25 08:03:25 +02:00
|
|
|
|
2019-05-05 23:17:36 +02:00
|
|
|
def get_transaction_height(self, tx_hash):
|
|
|
|
return self.rpc('blockchain.transaction.get_height', [tx_hash])
|
|
|
|
|
2018-05-25 08:03:25 +02:00
|
|
|
def get_merkle(self, tx_hash, height):
|
2019-04-07 00:08:33 +02:00
|
|
|
return self.rpc('blockchain.transaction.get_merkle', [tx_hash, height])
|
2018-05-25 08:03:25 +02:00
|
|
|
|
|
|
|
def get_headers(self, height, count=10000):
|
2019-04-07 00:08:33 +02:00
|
|
|
return self.rpc('blockchain.block.headers', [height, count])
|
2018-05-25 08:03:25 +02:00
|
|
|
|
|
|
|
def subscribe_headers(self):
|
2019-04-07 00:08:33 +02:00
|
|
|
return self.rpc('blockchain.headers.subscribe', [True])
|
2018-05-25 08:03:25 +02:00
|
|
|
|
|
|
|
def subscribe_address(self, address):
|
2019-04-07 00:08:33 +02:00
|
|
|
return self.rpc('blockchain.address.subscribe', [address])
|