├── testing └── README.md ├── .gitignore ├── requirements.txt ├── connectrum ├── __init__.py ├── exc.py ├── constants.py ├── protocol.py ├── findall.py ├── svr_info.py ├── client.py └── servers.json ├── MANIFEST.in ├── optional_requirements.txt ├── Makefile ├── LICENSE ├── setup.py ├── examples ├── subscribe.py ├── cli.py ├── spider.py └── explorer.py └── README.md /testing/README.md: -------------------------------------------------------------------------------- 1 | 2 | # TODO add py.test tests here... 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | ENV 4 | pp 5 | dist 6 | build 7 | connectrum.egg-info 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # no required packages, but see optional_requirements to enable more features 2 | -------------------------------------------------------------------------------- /connectrum/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .exc import ElectrumErrorResponse 3 | 4 | __version__ = '0.8.1' 5 | -------------------------------------------------------------------------------- /connectrum/exc.py: -------------------------------------------------------------------------------- 1 | # 2 | # Exceptions 3 | # 4 | 5 | 6 | class ElectrumErrorResponse(RuntimeError): 7 | pass 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include connectrum/servers.json 3 | recursive-exclude testing * 4 | recursive-exclude examples * 5 | -------------------------------------------------------------------------------- /optional_requirements.txt: -------------------------------------------------------------------------------- 1 | # for IRC poll of servers; somewhat optional? 2 | bottom>=1.0.2 3 | 4 | # for assess to TOR and other socks5 proxies 5 | aiosocks>=0.1.5 6 | 7 | # for examples/explorer.py 8 | aiohttp 9 | 10 | # for some wrapping/backwards compat 11 | # - only required if you call obsolete method, and we need to rework it 12 | pycoin>=0.90.20200322 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This Makefile is only useful for maintainers of this package. 2 | 3 | all: 4 | echo Targets: build, tag, upload 5 | 6 | .PHONY: build 7 | build: 8 | python3 setup.py sdist 9 | 10 | .PHONY: upload 11 | upload: FNAME := $(shell ls -1t dist/connectrum-*gz | head -1) 12 | upload: 13 | gpg -u 5A2A5B10 --detach-sign -a $(FNAME) 14 | twine upload $(FNAME)* 15 | 16 | .PHONY: tag 17 | tag: VER := $(shell python -c 'import connectrum; print(connectrum.__version__)') 18 | tag: 19 | git tag v$(VER) -am "New release" 20 | git push --tags 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 by Coinkite Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /connectrum/constants.py: -------------------------------------------------------------------------------- 1 | 2 | # copied values from electrum source 3 | 4 | # IDK, maybe? 5 | ELECTRUM_VERSION = '2.6.4' # version of the client package 6 | PROTOCOL_VERSION = '0.10' # protocol version requested 7 | 8 | # note: 'v' and 'p' are effectively reserved as well. 9 | PROTOCOL_CODES = dict(t='TCP (plaintext)', h='HTTP (plaintext)', s='SSL', g='Websocket') 10 | 11 | # from electrum/lib/network.py at Jun/2016 12 | # 13 | DEFAULT_PORTS = { 't':50001, 's':50002, 'h':8081, 'g':8082} 14 | 15 | BOOTSTRAP_SERVERS = { 16 | 'erbium1.sytes.net': {'t':50001, 's':50002}, 17 | 'ecdsa.net': {'t':50001, 's':110}, 18 | 'electrum0.electricnewyear.net': {'t':50001, 's':50002}, 19 | 'VPS.hsmiths.com': {'t':50001, 's':50002}, 20 | 'ELECTRUM.jdubya.info': {'t':50001, 's':50002}, 21 | 'electrum.no-ip.org': {'t':50001, 's':50002, 'g':443}, 22 | 'us.electrum.be': DEFAULT_PORTS, 23 | 'bitcoins.sk': {'t':50001, 's':50002}, 24 | 'electrum.petrkr.net': {'t':50001, 's':50002}, 25 | 'electrum.dragonzone.net': DEFAULT_PORTS, 26 | 'Electrum.hsmiths.com': {'t':8080, 's':995}, 27 | 'electrum3.hachre.de': {'t':50001, 's':50002}, 28 | 'elec.luggs.co': {'t':80, 's':443}, 29 | 'btc.smsys.me': {'t':110, 's':995}, 30 | 'electrum.online': {'t':50001, 's':50002}, 31 | } 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Setup file 3 | 4 | See https://packaging.python.org/tutorials/distributing-packages/ 5 | 6 | But basically: 7 | python3 setup.py sdist 8 | (that makes a new tgz in ./dist) 9 | gpg -u 5A2A5B10 --detach-sign -a dist/connectrum-XXX.tar.gz 10 | twine upload dist/connectrum-XXX.* 11 | git tag vXXXX -a "New release" 12 | git push --tags 13 | 14 | """ 15 | import os 16 | from setuptools import setup, find_packages 17 | 18 | HERE = os.path.abspath(os.path.dirname(__file__)) 19 | README = open(os.path.join(HERE, 'README.md')).read() 20 | 21 | 22 | def get_version(): 23 | with open("connectrum/__init__.py") as f: 24 | for line in f: 25 | if line.startswith("__version__"): 26 | return eval(line.split("=")[-1]) 27 | 28 | REQUIREMENTS = [ 29 | # none at this time 30 | ] 31 | 32 | TEST_REQUIREMENTS = [ 33 | 'aiohttp', 34 | 'pytest', 35 | 'tox', 36 | 'aiosocks', 37 | 'aiohttp', 38 | 'bottom>=1.0.2' 39 | ] 40 | 41 | if __name__ == "__main__": 42 | setup( 43 | name='connectrum', 44 | python_requires='>=3.6.0', 45 | version=get_version(), 46 | description="asyncio-based Electrum client library", 47 | long_description=README, 48 | classifiers=[ 49 | 'Development Status :: 4 - Beta', 50 | 'Intended Audience :: Developers', 51 | 'License :: OSI Approved :: MIT License', 52 | 'Operating System :: OS Independent', 53 | 'Programming Language :: Python', 54 | 'Programming Language :: Python :: 3', 55 | 'Programming Language :: Python :: 3.6', 56 | 'Topic :: Software Development :: Libraries', 57 | ], 58 | author='Peter Gray', 59 | author_email='peter@coinkite.com', 60 | url='https://github.com/coinkite/connectrum', 61 | license='MIT', 62 | keywords='electrum bitcoin asnycio client', 63 | platforms='any', 64 | include_package_data=True, 65 | packages=find_packages(exclude=('testing', 'examples')), 66 | #data_files=['connectrum/servers.json'], 67 | install_requires=REQUIREMENTS, 68 | tests_require=REQUIREMENTS + TEST_REQUIREMENTS, 69 | ) 70 | -------------------------------------------------------------------------------- /connectrum/protocol.py: -------------------------------------------------------------------------------- 1 | # 2 | # Implement an asyncio.Protocol for Electrum (clients) 3 | # 4 | # 5 | import asyncio, json 6 | import logging 7 | 8 | logger = logging.getLogger('connectrum') 9 | 10 | class StratumProtocol(asyncio.Protocol): 11 | client = None 12 | closed = False 13 | transport = None 14 | buf = b"" 15 | 16 | def connection_made(self, transport): 17 | self.transport = transport 18 | logger.debug("Transport connected ok") 19 | 20 | def connection_lost(self, exc): 21 | if not self.closed: 22 | self.closed = True 23 | self.close() 24 | self.client._connection_lost(self) 25 | 26 | def data_received(self, data): 27 | self.buf += data 28 | 29 | # Unframe the mesage. Expecting JSON. 30 | *lines, self.buf = self.buf.split(b'\n') 31 | 32 | for line in lines: 33 | if not line: continue 34 | 35 | try: 36 | msg = line.decode('utf-8', "error").strip() 37 | except UnicodeError as exc: 38 | logger.exception("Encoding issue on %r" % line) 39 | self.connection_lost(exc) 40 | return 41 | 42 | try: 43 | msg = json.loads(msg) 44 | except ValueError as exc: 45 | logger.exception("Bad JSON received from server: %r" % msg) 46 | self.connection_lost(exc) 47 | return 48 | 49 | #logger.debug("RX:\n%s", json.dumps(msg, indent=2)) 50 | 51 | try: 52 | self.client._got_response(msg) 53 | except Exception as e: 54 | logger.exception("Trouble handling response! (%s)" % e) 55 | continue 56 | 57 | def send_data(self, message): 58 | ''' 59 | Given an object, encode as JSON and transmit to the server. 60 | ''' 61 | #logger.debug("TX:\n%s", json.dumps(message, indent=2)) 62 | data = json.dumps(message).encode('utf-8') + b'\n' 63 | self.transport.write(data) 64 | 65 | def close(self): 66 | if not self.closed: 67 | try: 68 | self.transport.close() 69 | finally: 70 | self.closed = True 71 | 72 | # EOF 73 | -------------------------------------------------------------------------------- /examples/subscribe.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # 3 | # Subscribe to any message stream that the server supports. 4 | # 5 | import sys, asyncio, argparse, json 6 | from connectrum.client import StratumClient 7 | from connectrum.svr_info import ServerInfo 8 | 9 | 10 | async def listen(conn, svr, connector, method, args, verbose=0): 11 | 12 | try: 13 | await connector 14 | except Exception as e: 15 | print("Unable to connect to server: %s" % e) 16 | return -1 17 | 18 | print("\nConnected to: %s\n" % svr) 19 | 20 | if verbose: 21 | donate = await conn.RPC('server.donation_address') 22 | if donate: 23 | print("Donations: " + donate) 24 | 25 | motd = await conn.RPC('server.banner') 26 | print("\n---\n%s\n---" % motd) 27 | 28 | print("\nMethod: %s" % method) 29 | 30 | fut, q = conn.subscribe(method, *args) 31 | print(json.dumps(await fut, indent=1)) 32 | while 1: 33 | result = await q.get() 34 | print(json.dumps(result, indent=1)) 35 | 36 | 37 | 38 | def main(): 39 | parser = argparse.ArgumentParser(description='Subscribe to BTC events') 40 | parser.add_argument('method', 41 | help='"blockchain.headers.subscribe" or similar') 42 | parser.add_argument('args', nargs="*", default=[], 43 | help='Arguments for method') 44 | parser.add_argument('--server', default='cluelessperson.com', 45 | help='Hostname of Electrum server to use') 46 | parser.add_argument('--protocol', default='s', 47 | help='Protocol code: t=TCP Cleartext, s=SSL, etc') 48 | parser.add_argument('--port', default=None, 49 | help='Port number to override default for protocol') 50 | parser.add_argument('--tor', default=False, action="store_true", 51 | help='Use local Tor proxy to connect') 52 | 53 | args = parser.parse_args() 54 | 55 | # convert to our datastruct about servers. 56 | svr = ServerInfo(args.server, args.server, 57 | ports=((args.protocol+str(args.port)) if args.port else args.protocol)) 58 | 59 | loop = asyncio.get_event_loop() 60 | 61 | conn = StratumClient() 62 | connector = conn.connect(svr, args.protocol, use_tor=svr.is_onion, disable_cert_verify=True) 63 | 64 | loop.run_until_complete(listen(conn, svr, connector, args.method, args.args)) 65 | 66 | loop.close() 67 | 68 | if __name__ == '__main__': 69 | main() 70 | 71 | # EOF 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Connectrum 2 | ---------- 3 | 4 | Stratum (electrum-server) Client Protocol library 5 | ================================================= 6 | 7 | Uses python3 to be a client to the Electrum server network. It makes heavy use of 8 | `asyncio` module and newer Python 3 keywords such as `await` and `async`. 9 | 10 | For non-server applications, you can probably find all you need 11 | already in the standard Electrum code and command line. 12 | 13 | Features 14 | ======== 15 | 16 | - can connect via Tor, SSL, proxied or directly 17 | - filter lists of peers by protocol, `.onion` name 18 | - manage lists of Electrum servers in simple JSON files. 19 | - fully asynchronous design, so can connect to multiple at once 20 | - a number of nearly-useful examples provided 21 | - any call to methods `blockchain.address.*` is converted into the more 22 | modern equivilent `blockchain.scripthash.*` transparently. Requires pycoin module. 23 | 24 | Examples 25 | ======== 26 | 27 | In `examples` you will find a number little example programs. 28 | 29 | - `cli.py` send single commands, plan is to make this an interactive REPL 30 | - `subscribe.py` stream changes/events for an address or blocks. 31 | - `explorer.py` implements a simplistic block explorer website 32 | - `spider.py` find all Electrum servers recursively, read/write results to JSON 33 | 34 | Version History 35 | =============== 36 | 37 | - **0.8.1** Handle protocol version reporting correctly, use 'ping' msg. (Says we are 1.4) 38 | - **0.8.0** Support for ElectrumX protocol 1.4 with some helpers to restore useful functions. 39 | - **0.7.4** Add `actual_connection` atrribute on `StratumClient` with some key details 40 | - **0.7.3** Not sure 41 | - **0.7.2** Bugfix: port numbers vs. protocols 42 | - **0.7.1** Python 2.6 compat fix 43 | - **0.7.0** Reconnect broken server connections automatically (after first connect). 44 | - **0.6.0** Various pull requests from other devs integrated. Thanks to @devrandom, @ysangkok! 45 | - **0.5.3** Documents the build/release process (no functional changes). 46 | - **0.5.2** Make aiosocks and bottom modules optional at runtime (thanks to @BioMike) 47 | - **0.5.1** Minor bug fixes 48 | - **0.5.0** First public release. 49 | 50 | 51 | TODO List 52 | ========= 53 | 54 | - be more robust about failed servers, reconnect and handle it. 55 | - connect to a few (3?) servers and compare top block and response times; pick best 56 | - some sort of persistant server list that can be updated as we run 57 | - type checking of parameters sent to server (maybe)? 58 | - lots of test code 59 | - an example that finds servers that do SSL with self-signed certificate 60 | - an example that fingerprints servers to learn what codebase they use 61 | - some bitcoin-specific code that all clients would need; like block header to hash 62 | -------------------------------------------------------------------------------- /examples/cli.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # 3 | # Provide an interactive command line for sending 4 | # commands to an Electrum server. 5 | # 6 | # TODO: finish this with interactive readline 7 | # 8 | import sys, asyncio, argparse, json 9 | from connectrum.client import StratumClient 10 | from connectrum.svr_info import ServerInfo 11 | from connectrum import ElectrumErrorResponse 12 | import logging 13 | 14 | logging.basicConfig(level=logging.INFO) 15 | #logging.getLogger('connectrum').setLevel(level=logging.DEBUG) 16 | 17 | async def interact(conn, svr, connector, method, args, verbose=False): 18 | 19 | try: 20 | await connector 21 | except Exception as e: 22 | print("Unable to connect to server: %s" % e) 23 | return -1 24 | 25 | print("\nConnected to: %s" % svr) 26 | print("Server version: %s" % conn.server_version) 27 | print("Server protocol: %s\n" % conn.protocol_version) 28 | 29 | if verbose: 30 | donate = await conn.RPC('server.donation_address') 31 | if donate: 32 | print("Donations: " + donate) 33 | 34 | motd = await conn.RPC('server.banner') 35 | print("\n---\n%s\n---" % motd) 36 | 37 | # XXX TODO do a simple REPL here 38 | 39 | if method: 40 | print("\nMethod: %s" % method) 41 | 42 | # risky type cocerce here 43 | args = [(int(i) if i.isdigit() else i) for i in args] 44 | 45 | try: 46 | rv = await conn.RPC(method, *args) 47 | print(json.dumps(rv, indent=1)) 48 | except ElectrumErrorResponse as e: 49 | print(e) 50 | 51 | conn.close() 52 | 53 | 54 | def main(): 55 | parser = argparse.ArgumentParser(description='Interact with an electrum server') 56 | parser.add_argument('method', default=None, 57 | help='"blockchain.numblocks.subscribe" or similar') 58 | parser.add_argument('args', nargs="*", default=[], 59 | help='Arguments for method') 60 | parser.add_argument('--server', default='cluelessperson.com', 61 | help='Hostname of Electrum server to use') 62 | parser.add_argument('--protocol', default='s', 63 | help='Protocol code: t=TCP Cleartext, s=SSL, etc') 64 | parser.add_argument('--port', default=None, 65 | help='Port number to override default for protocol') 66 | parser.add_argument('--tor', default=False, action="store_true", 67 | help='Use local Tor proxy to connect') 68 | 69 | args = parser.parse_args() 70 | 71 | import logging 72 | 73 | # convert to our datastruct about servers. 74 | svr = ServerInfo(args.server, args.server, 75 | ports=((args.protocol+str(args.port)) if args.port else args.protocol)) 76 | 77 | loop = asyncio.get_event_loop() 78 | 79 | conn = StratumClient() 80 | connector = conn.connect(svr, args.protocol, use_tor=svr.is_onion, disable_cert_verify=True, short_term=True) 81 | 82 | loop.run_until_complete(interact(conn, svr, connector, args.method, args.args)) 83 | 84 | loop.close() 85 | 86 | if __name__ == '__main__': 87 | main() 88 | 89 | # EOF 90 | -------------------------------------------------------------------------------- /examples/spider.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # 3 | # Find all Electrum servers, everywhere... It will connect to one at random (from 4 | # a hard-coded list) and then expand it's list of peers based on what it sees 5 | # at each server. 6 | # 7 | # THIS IS A DEMO PROGRAM ONLY. It would be anti-social to run this frequently or 8 | # as part of any periodic task. 9 | # 10 | import sys, asyncio, argparse 11 | from connectrum.client import StratumClient 12 | from connectrum.svr_info import KnownServers 13 | 14 | ks = KnownServers() 15 | 16 | connected = set() 17 | failed = set() 18 | 19 | async def probe(svr, proto_code, use_tor): 20 | conn = StratumClient() 21 | 22 | try: 23 | await conn.connect(svr, proto_code, use_tor=(svr.is_onion or use_tor), short_term=True) 24 | except: 25 | failed.add(str(svr)) 26 | return None 27 | 28 | peers, _ = conn.subscribe('server.peers.subscribe') 29 | 30 | peers = await peers 31 | print("%s gave %d peers" % (svr, len(peers))) 32 | 33 | connected.add(str(svr)) 34 | 35 | # track them all. 36 | more = ks.add_peer_response(peers) 37 | 38 | if more: 39 | print("found %d more servers from %s: %s" % (len(more), svr, ', '.join(more))) 40 | 41 | 42 | conn.close() 43 | 44 | return str(svr) 45 | 46 | 47 | if __name__ == '__main__': 48 | 49 | parser = argparse.ArgumentParser(description='Interact with an electrum server') 50 | 51 | parser.add_argument('servers', default=[], metavar="server_list.json", nargs='*', 52 | help='JSON file containing server details') 53 | parser.add_argument('--protocol', default='t', choices='ts', 54 | help='Protocol code: t=TCP Cleartext, s=SSL, etc') 55 | parser.add_argument('--tor', default=False, action="store_true", 56 | help='Use local Tor proxy to connect (localhost:9150)') 57 | parser.add_argument('--onion', default=None, action="store_true", 58 | help='Select only servers operating an .onion name') 59 | parser.add_argument('--irc', default=False, action="store_true", 60 | help='Use IRC channel to find servers') 61 | parser.add_argument('--output', default=None, 62 | help='File to save resulting server list into (JSON)') 63 | parser.add_argument('--timeout', default=30, type=int, 64 | help='Total time to take (overall)') 65 | 66 | args = parser.parse_args() 67 | 68 | if args.irc: 69 | print("Connecting to freenode #electrum... (slow, be patient)") 70 | ks.from_irc() 71 | 72 | for a in args.servers: 73 | ks.from_json(a) 74 | 75 | #ks.from_json('../connectrum/servers.json') 76 | 77 | if not ks: 78 | print("Please use --irc option or a list of servers in JSON on command line") 79 | sys.exit(1) 80 | 81 | print("%d servers are known to us at start" % len(ks)) 82 | 83 | loop = asyncio.get_event_loop() 84 | 85 | # cannot reach .onion if not using Tor; so filter them out 86 | if not args.tor: 87 | args.onion = False 88 | 89 | candidates = ks.select(protocol=args.protocol, is_onion=args.onion) 90 | print("%d servers are right protocol" % len(candidates)) 91 | 92 | all_done = asyncio.wait([probe(i, args.protocol, args.tor) for i in candidates], 93 | timeout=args.timeout) 94 | 95 | loop.run_until_complete(all_done) 96 | loop.close() 97 | 98 | if not connected: 99 | print("WARNING: did not successfully connect to any existing servers!") 100 | else: 101 | print("%d servers connected and answered correctly" % len(connected)) 102 | 103 | if failed: 104 | print("%d FAILURES: " % len(failed)) 105 | for i in failed: 106 | print(' %s' % i) 107 | 108 | print("%d servers are now known" % len(ks)) 109 | if 0: 110 | for i in ks.values(): 111 | print(' %s [%s]' % (i.hostname, ' '.join(i.protocols))) 112 | 113 | if args.output: 114 | ks.save_json(args.output) 115 | 116 | # EOF 117 | -------------------------------------------------------------------------------- /connectrum/findall.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # 4 | import bottom, random, time, asyncio 5 | from .svr_info import ServerInfo 6 | import logging 7 | 8 | logger = logging.getLogger('connectrum') 9 | 10 | class IrcListener(bottom.Client): 11 | def __init__(self, irc_nickname=None, irc_password=None, ssl=True): 12 | self.my_nick = irc_nickname or 'XC%d' % random.randint(1E11, 1E12) 13 | self.password = irc_password or None 14 | 15 | self.results = {} # by hostname 16 | self.servers = set() 17 | self.all_done = asyncio.Event() 18 | 19 | super(IrcListener, self).__init__(host='irc.freenode.net', port=6697 if ssl else 6667, ssl=ssl) 20 | 21 | # setup event handling 22 | self.on('CLIENT_CONNECT', self.connected) 23 | self.on('PING', self.keepalive) 24 | self.on('JOIN', self.joined) 25 | self.on('RPL_NAMREPLY', self.got_users) 26 | self.on('RPL_WHOREPLY', self.got_who_reply) 27 | self.on("client_disconnect", self.reconnect) 28 | self.on('RPL_ENDOFNAMES', self.got_end_of_names) 29 | 30 | async def collect_data(self): 31 | # start it process 32 | self.loop.create_task(self.connect()) 33 | 34 | # wait until done 35 | await self.all_done.wait() 36 | 37 | # return the results 38 | return self.results 39 | 40 | def connected(self, **kwargs): 41 | logger.debug("Connected") 42 | self.send('NICK', nick=self.my_nick) 43 | self.send('USER', user=self.my_nick, realname='Connectrum Client') 44 | # long delay here as it does an failing Ident probe (10 seconds min) 45 | self.send('JOIN', channel='#electrum') 46 | #self.send('WHO', mask='E_*') 47 | 48 | def keepalive(self, message, **kwargs): 49 | self.send('PONG', message=message) 50 | 51 | async def joined(self, nick=None, **kwargs): 52 | # happens when we or someone else joins the channel 53 | # seem to take 10 seconds or longer for me to join 54 | logger.debug('Joined: %r' % kwargs) 55 | 56 | if nick != self.my_nick: 57 | await self.add_server(nick) 58 | 59 | async def got_who_reply(self, nick=None, real_name=None, **kws): 60 | ''' 61 | Server replied to one of our WHO requests, with details. 62 | ''' 63 | #logger.debug('who reply: %r' % kws) 64 | 65 | nick = nick[2:] if nick[0:2] == 'E_' else nick 66 | host, ports = real_name.split(' ', 1) 67 | 68 | self.servers.remove(nick) 69 | 70 | logger.debug("Found: '%s' at %s with port list: %s",nick, host, ports) 71 | self.results[host.lower()] = ServerInfo(nick, host, ports) 72 | 73 | if not self.servers: 74 | self.all_done.set() 75 | 76 | async def got_users(self, users=[], **kws): 77 | # After successful join to channel, we are given a list of 78 | # users on the channel. Happens a few times for busy channels. 79 | logger.debug('Got %d (more) users in channel', len(users)) 80 | 81 | for nick in users: 82 | await self.add_server(nick) 83 | 84 | async def add_server(self, nick): 85 | # ignore everyone but electrum servers 86 | if nick.startswith('E_'): 87 | self.servers.add(nick[2:]) 88 | 89 | async def who_worker(self): 90 | # Fetch details on each Electrum server nick we see 91 | logger.debug('who task starts') 92 | copy = self.servers.copy() 93 | for nn in copy: 94 | logger.debug('do WHO for: ' + nn) 95 | self.send('WHO', mask='E_'+nn) 96 | 97 | logger.debug('who task done') 98 | 99 | def got_end_of_names(self, *a, **k): 100 | logger.debug('Got all the user names') 101 | 102 | assert self.servers, "No one on channel!" 103 | 104 | # ask for details on all of those users 105 | self.loop.create_task(self.who_worker()) 106 | 107 | 108 | async def reconnect(self, **kwargs): 109 | # Trigger an event that may cascade to a client_connect. 110 | # Don't continue until a client_connect occurs, which may be never. 111 | 112 | logger.warn("Disconnected (will reconnect)") 113 | 114 | # Note that we're not in a coroutine, so we don't have access 115 | # to await and asyncio.sleep 116 | time.sleep(3) 117 | 118 | # After this line we won't necessarily be connected. 119 | # We've simply scheduled the connect to happen in the future 120 | self.loop.create_task(self.connect()) 121 | 122 | logger.debug("Reconnect scheduled.") 123 | 124 | 125 | if __name__ == '__main__': 126 | 127 | 128 | import logging 129 | logging.getLogger('bottom').setLevel(logging.DEBUG) 130 | logging.getLogger('connectrum').setLevel(logging.DEBUG) 131 | logging.getLogger('asyncio').setLevel(logging.DEBUG) 132 | 133 | 134 | bot = IrcListener(ssl=False) 135 | bot.loop.set_debug(True) 136 | fut = bot.collect_data() 137 | #bot.loop.create_task(bot.connect()) 138 | rv = bot.loop.run_until_complete(fut) 139 | 140 | print(rv) 141 | 142 | -------------------------------------------------------------------------------- /examples/explorer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Be a simple bitcoin block explorer. Just an toy example! 4 | # 5 | # Limitations: 6 | # - picks a random Electrum server each time it starts (which is a crapshoot) 7 | # - displays nothing interesting for txn 8 | # - does not do block hash numbers, only by height 9 | # - inline html is terrible 10 | # - ugly 11 | # 12 | import re, aiohttp, json, textwrap, asyncio, sys 13 | from aiohttp import web 14 | from aiohttp.web import HTTPFound, Response 15 | from connectrum.client import StratumClient 16 | from connectrum.svr_info import KnownServers, ServerInfo 17 | from connectrum import ElectrumErrorResponse 18 | 19 | top_blk = 6666 20 | 21 | HTML_HDR = ''' 22 | 23 | 26 | 27 |

Explore Bitcoin

28 | ''' 29 | 30 | def linkage(n, label=None): 31 | n = str(n) 32 | if len(n) == 64: 33 | t = 'txn' 34 | elif len(n) < 7: 35 | t = 'blk' 36 | else: 37 | t = 'addr' 38 | 39 | return '%s' % (t, n, label or n) 40 | 41 | 42 | async def homepage(request): 43 | conn = request.app['conn'] 44 | t = HTML_HDR 45 | t += "

%s

" % conn.server_info 46 | 47 | # NOTE: only a demo program would do all these remote server 48 | # queries just to display the hompage... 49 | 50 | donate = await conn.RPC('server.donation_address') 51 | 52 | motd = await conn.RPC('server.banner') 53 | t += '
\n%s\n

' % motd 54 | 55 | t += '

Donations: %s

' % linkage(donate) 56 | t += '

Top block: %s

' % linkage(top_blk['block_height']) 57 | 58 | 59 | t += ''' 60 |
61 | 62 | ''' 63 | 64 | return Response(content_type='text/html', text=t) 65 | 66 | async def call_and_format(conn, method, *args): 67 | # call a method and format up the response nicely 68 | 69 | t = '' 70 | try: 71 | resp = await conn.RPC(method, *args) 72 | except ElectrumErrorResponse as e: 73 | response, req = e.args 74 | t += "

Server Error

%s\n%s
" % (response, req) 75 | return t 76 | 77 | if isinstance(resp, str): 78 | here = '\n'.join(textwrap.wrap(resp)) 79 | else: 80 | here = json.dumps(resp, indent=2) 81 | 82 | # simulate
 somewhat
 83 |     here = here.replace('\n', '
') 84 | here = here.replace('
', '
 ') 85 | 86 | # link to txn 87 | here = re.sub(r'"([a-f0-9]{64})"', lambda m: linkage(m.group(1)), here) 88 | # TODO: link to blk numbers 89 | 90 | 91 | t += '

%s

' % method 92 | t += here 93 | t += '' 94 | 95 | return t 96 | 97 | async def search(request): 98 | query = (await request.post())['q'].strip() 99 | 100 | if not (1 <= len(query) <= 200): 101 | raise HTTPFound('/') 102 | 103 | if len(query) <= 7: 104 | raise HTTPFound('/blk/'+query.lower()) 105 | elif len(query) == 64: 106 | # assume it's a hash of block or txn 107 | raise HTTPFound('/txn/'+query.lower()) 108 | elif query[0] in '13mn': 109 | # assume it'a payment address 110 | raise HTTPFound('/addr/'+query) 111 | else: 112 | return Response(text="Can't search for that") 113 | 114 | 115 | async def address_page(request): 116 | # address summary by bitcoin payment addr 117 | addr = request.match_info['addr'] 118 | conn = request.app['conn'] 119 | 120 | t = HTML_HDR 121 | t += '

%s

' % addr 122 | 123 | for method in ['blockchain.address.get_balance', 124 | #'blockchain.address.get_status', 125 | 'blockchain.address.get_mempool', 126 | #'blockchain.address.get_proof', 127 | 'blockchain.address.listunspent']: 128 | # get a balance, etc. 129 | t += await call_and_format(conn, method, addr) 130 | 131 | return Response(content_type='text/html', text=t) 132 | 133 | async def transaction_page(request): 134 | # transaction by hash 135 | txn_hash = request.match_info['txn_hash'] 136 | conn = request.app['conn'] 137 | 138 | t = HTML_HDR 139 | t += '

%s

' % txn_hash 140 | 141 | for method in ['blockchain.transaction.get']: 142 | t += await call_and_format(conn, method, txn_hash) 143 | 144 | return Response(content_type='text/html', text=t) 145 | 146 | async def block_page(request): 147 | # blocks by number (height) 148 | height = int(request.match_info['height'], 10) 149 | conn = request.app['conn'] 150 | 151 | t = HTML_HDR 152 | t += '

Block %d

' % height 153 | 154 | for method in ['blockchain.block.get_header']: 155 | t += await call_and_format(conn, method, height) 156 | 157 | t += '

%s    %s

' % (linkage(height-1, "PREV"), linkage(height+1, "NEXT")) 158 | 159 | return Response(content_type='text/html', text=t) 160 | 161 | async def startup_code(app): 162 | # pick a random server 163 | app['conn'] = conn = StratumClient() 164 | try: 165 | await conn.connect(el_server, disable_cert_verify=True, 166 | use_tor=('localhost', 9150) if el_server.is_onion else False) 167 | except Exception as exc: 168 | print("unable to connect: %r" % exc) 169 | sys.exit() 170 | 171 | print("Connected to electrum server: {hostname}:{port} ssl={ssl} tor={tor} ip_addr={ip_addr}".format(**conn.actual_connection)) 172 | 173 | # track top block 174 | async def track_top_block(): 175 | global top_blk 176 | fut, Q = conn.subscribe('blockchain.headers.subscribe') 177 | top_blk = await fut 178 | while 1: 179 | top_blk = max(await Q.get()) 180 | print("new top-block: %r" % (top_blk,)) 181 | 182 | app.loop.create_task(track_top_block()) 183 | 184 | 185 | if __name__ == "__main__": 186 | app = web.Application() 187 | app.router.add_route('GET', '/', homepage) 188 | app.router.add_route('POST', '/', search) 189 | app.router.add_route('GET', '/addr/{addr}', address_page) 190 | app.router.add_route('GET', '/txn/{txn_hash}', transaction_page) 191 | app.router.add_route('GET', '/blk/{height}', block_page) 192 | 193 | if 0: 194 | ks = KnownServers() 195 | ks.from_json('../connectrum/servers.json') 196 | servers = ks.select(is_onion=False, min_prune=1000) 197 | 198 | assert servers, "Need some servers to talk to." 199 | el_server = servers[0] 200 | else: 201 | el_server = ServerInfo('hardcoded', sys.argv[-1], 's') 202 | #el_server = ServerInfo('hardcoded', 'daedalus.bauerj.eu', 's') 203 | 204 | loop = asyncio.get_event_loop() 205 | loop.create_task(startup_code(app)) 206 | 207 | web.run_app(app) 208 | -------------------------------------------------------------------------------- /connectrum/svr_info.py: -------------------------------------------------------------------------------- 1 | # 2 | # Store information about servers. Filter and select based on their protocol support, etc. 3 | # 4 | 5 | # Runtime check for optional modules 6 | from importlib import util as importutil 7 | 8 | # Check if bottom is present. 9 | if importutil.find_spec("bottom") is not None: 10 | have_bottom = True 11 | else: 12 | have_bottom = False 13 | 14 | import time, random, json 15 | from .constants import DEFAULT_PORTS 16 | 17 | 18 | class ServerInfo(dict): 19 | ''' 20 | Information to be stored on a server. Originally based on IRC data published to a channel. 21 | 22 | ''' 23 | FIELDS = ['nickname', 'hostname', 'ports', 'version', 'pruning_limit' ] 24 | 25 | def __init__(self, nickname_or_dict, hostname=None, ports=None, 26 | version=None, pruning_limit=None, ip_addr=None): 27 | 28 | if not hostname and not ports: 29 | # promote a dict, or similar 30 | super(ServerInfo, self).__init__(nickname_or_dict) 31 | return 32 | 33 | self['nickname'] = nickname_or_dict or None 34 | self['hostname'] = hostname 35 | self['ip_addr'] = ip_addr or None 36 | 37 | # For 'ports', take 38 | # - a number (int), assumed to be TCP port, OR 39 | # - a list of codes, OR 40 | # - a string to be split apart. 41 | # Keep version and pruning limit separate 42 | # 43 | if isinstance(ports, int): 44 | ports = ['t%d' % ports] 45 | elif isinstance(ports, str): 46 | ports = ports.split() 47 | 48 | # check we don't have junk in the ports list 49 | for p in ports.copy(): 50 | if p[0] == 'v': 51 | version = p[1:] 52 | ports.remove(p) 53 | elif p[0] == 'p': 54 | try: 55 | pruning_limit = int(p[1:]) 56 | except ValueError: 57 | # ignore junk 58 | pass 59 | ports.remove(p) 60 | 61 | assert ports, "Must have at least one port/protocol" 62 | 63 | self['ports'] = ports 64 | self['version'] = version 65 | self['pruning_limit'] = int(pruning_limit or 0) 66 | 67 | @classmethod 68 | def from_response(cls, response_list): 69 | # Create a list of servers based on data from response from Stratum. 70 | # Give this the response to: "server.peers.subscribe" 71 | # 72 | # [... 73 | # "91.63.237.12", 74 | # "electrum3.hachre.de", 75 | # [ "v1.0", "p10000", "t", "s" ] 76 | # ...] 77 | # 78 | rv = [] 79 | 80 | for params in response_list: 81 | ip_addr, hostname, ports = params 82 | 83 | if ip_addr == hostname: 84 | ip_addr = None 85 | 86 | rv.append(ServerInfo(None, hostname, ports, ip_addr=ip_addr)) 87 | 88 | return rv 89 | 90 | @classmethod 91 | def from_dict(cls, d): 92 | n = d.pop('nickname', None) 93 | h = d.pop('hostname') 94 | p = d.pop('ports') 95 | rv = cls(n, h, p) 96 | rv.update(d) 97 | return rv 98 | 99 | 100 | @property 101 | def protocols(self): 102 | rv = set(i[0] for i in self['ports']) 103 | assert 'p' not in rv, 'pruning limit got in there' 104 | assert 'v' not in rv, 'version got in there' 105 | return rv 106 | 107 | @property 108 | def pruning_limit(self): 109 | return self.get('pruning_limit', 100) 110 | 111 | @property 112 | def hostname(self): 113 | return self.get('hostname') 114 | 115 | def get_port(self, for_protocol): 116 | ''' 117 | Return (hostname, port number, ssl) pair for the protocol. 118 | Assuming only one port per host. 119 | ''' 120 | assert len(for_protocol) == 1, "expect single letter code" 121 | 122 | use_ssl = for_protocol in ('s', 'g') 123 | 124 | if 'port' in self: return self['hostname'], int(self['port']), use_ssl 125 | 126 | rv = next(i for i in self['ports'] if i[0] == for_protocol) 127 | 128 | port = None 129 | if len(rv) >= 2: 130 | try: 131 | port = int(rv[1:]) 132 | except: 133 | pass 134 | port = port or DEFAULT_PORTS[for_protocol] 135 | 136 | return self['hostname'], port, use_ssl 137 | 138 | @property 139 | def is_onion(self): 140 | return self['hostname'].lower().endswith('.onion') 141 | 142 | def select(self, protocol='s', is_onion=None, min_prune=0): 143 | # predicate function for selection based on features/properties 144 | return ((protocol in self.protocols) 145 | and (self.is_onion == is_onion if is_onion is not None else True) 146 | and (self.pruning_limit >= min_prune)) 147 | 148 | def __repr__(self): 149 | return ''\ 150 | .format(**self) 151 | 152 | def __str__(self): 153 | # used as a dict key in a few places. 154 | return self['hostname'].lower() 155 | 156 | def __hash__(self): 157 | # this one-line allows use as a set member, which is really handy! 158 | return hash(self['hostname'].lower()) 159 | 160 | class KnownServers(dict): 161 | ''' 162 | Store a list of known servers and their port numbers, etc. 163 | 164 | - can add single entries 165 | - can read from a CSV for seeding/bootstrap 166 | - can read from IRC channel to find current hosts 167 | 168 | We are a dictionary, with key being the hostname (in lowercase) of the server. 169 | ''' 170 | 171 | def from_json(self, fname): 172 | ''' 173 | Read contents of a CSV containing a list of servers. 174 | ''' 175 | with open(fname, 'rt') as fp: 176 | for row in json.load(fp): 177 | nn = ServerInfo.from_dict(row) 178 | self[str(nn)] = nn 179 | 180 | def from_irc(self, irc_nickname=None, irc_password=None): 181 | ''' 182 | Connect to the IRC channel and find all servers presently connected. 183 | 184 | Slow; takes 30+ seconds but authoritative and current. 185 | 186 | OBSOLETE. 187 | ''' 188 | if have_bottom: 189 | from .findall import IrcListener 190 | 191 | # connect and fetch current set of servers who are 192 | # on #electrum channel at freenode 193 | 194 | bot = IrcListener(irc_nickname=irc_nickname, irc_password=irc_password) 195 | results = bot.loop.run_until_complete(bot.collect_data()) 196 | bot.loop.close() 197 | 198 | # merge by nick name 199 | self.update(results) 200 | else: 201 | return(False) 202 | 203 | def add_single(self, hostname, ports, nickname=None, **kws): 204 | ''' 205 | Explicitly add a single entry. 206 | Hostname is a FQDN and ports is either a single int (assumed to be TCP port) 207 | or Electrum protocol/port number specification with spaces in between. 208 | ''' 209 | nickname = nickname or hostname 210 | 211 | self[hostname.lower()] = ServerInfo(nickname, hostname, ports, **kws) 212 | 213 | def add_peer_response(self, response_list): 214 | # Update with response from Stratum (lacks the nickname value tho): 215 | # 216 | # "91.63.237.12", 217 | # "electrum3.hachre.de", 218 | # [ "v1.0", "p10000", "t", "s" ] 219 | # 220 | additions = set() 221 | for params in response_list: 222 | ip_addr, hostname, ports = params 223 | 224 | if ip_addr == hostname: 225 | ip_addr = None 226 | 227 | g = self.get(hostname.lower()) 228 | nickname = g['nickname'] if g else None 229 | 230 | here = ServerInfo(nickname, hostname, ports, ip_addr=ip_addr) 231 | self[str(here)] = here 232 | 233 | if not g: 234 | additions.add(str(here)) 235 | 236 | return additions 237 | 238 | def save_json(self, fname='servers.json'): 239 | ''' 240 | Write out to a CSV file. 241 | ''' 242 | rows = sorted(self.keys()) 243 | with open(fname, 'wt') as fp: 244 | json.dump([self[k] for k in rows], fp, indent=1) 245 | 246 | def dump(self): 247 | return '\n'.join(repr(i) for i in self.values()) 248 | 249 | def select(self, **kws): 250 | ''' 251 | Find all servers with indicated protocol support. Shuffled. 252 | 253 | Filter by TOR support, and pruning level. 254 | ''' 255 | lst = [i for i in self.values() if i.select(**kws)] 256 | 257 | random.shuffle(lst) 258 | 259 | return lst 260 | 261 | 262 | if __name__ == '__main__': 263 | 264 | ks = KnownServers() 265 | 266 | #ks.from_json('servers.json') 267 | ks.from_irc() 268 | 269 | #print (ks.dump()) 270 | 271 | from constants import PROTOCOL_CODES 272 | 273 | print ("%3d: servers in total" % len(ks)) 274 | 275 | for tor in [False, True]: 276 | for pp in PROTOCOL_CODES.keys(): 277 | ll = ks.select(pp, is_onion=tor) 278 | print ("%3d: %s" % (len(ll), PROTOCOL_CODES[pp] + (' [TOR]' if tor else ''))) 279 | 280 | # EOF 281 | -------------------------------------------------------------------------------- /connectrum/client.py: -------------------------------------------------------------------------------- 1 | # 2 | # Client connect to an Electrum server. 3 | # 4 | 5 | # Runtime check for optional modules 6 | from importlib import util as importutil 7 | import json, warnings, asyncio, ssl 8 | from .protocol import StratumProtocol 9 | from . import __version__ 10 | 11 | # Check if aiosocks is present, and load it if it is. 12 | if importutil.find_spec("aiosocks") is not None: 13 | import aiosocks 14 | have_aiosocks = True 15 | else: 16 | have_aiosocks = False 17 | 18 | from collections import defaultdict 19 | from .exc import ElectrumErrorResponse 20 | import logging 21 | 22 | logger = logging.getLogger('connectrum') 23 | 24 | class StratumClient: 25 | 26 | 27 | def __init__(self, loop=None): 28 | ''' 29 | Setup state needed to handle req/resp from a single Stratum server. 30 | Requires a transport (TransportABC) object to do the communication. 31 | ''' 32 | self.protocol = None 33 | 34 | self.next_id = 1 35 | self.inflight = {} 36 | self.subscriptions = defaultdict(list) 37 | 38 | # report our version, honestly; and indicate we only understand 1.4 39 | self.my_version_args = (f'Connectrum/{__version__}', '1.4') 40 | 41 | # these are valid after connection 42 | self.server_version = None # 'ElectrumX 1.13.0' or similar 43 | self.protocol_version = None # float(1.4) or similar 44 | 45 | self.actual_connection = {} 46 | 47 | self.ka_task = None 48 | 49 | self.loop = loop or asyncio.get_event_loop() 50 | 51 | self.reconnect = None # call connect() first 52 | 53 | # next step: call connect() 54 | 55 | def _connection_lost(self, protocol): 56 | # Ignore connection_lost for old connections 57 | self.disconnect_callback and self.disconnect_callback(self) 58 | if protocol is not self.protocol: 59 | return 60 | 61 | self.protocol = None 62 | logger.warn("Electrum server connection lost") 63 | 64 | for (_, fut) in self.inflight.values(): 65 | fut.set_exception(ElectrumErrorResponse("Electrum server connection lost")) 66 | 67 | self.inflight.clear() 68 | 69 | # cleanup keep alive task 70 | if self.ka_task: 71 | self.ka_task.cancel() 72 | self.ka_task = None 73 | 74 | def close(self): 75 | if self.protocol: 76 | self.protocol.close() 77 | self.protocol = None 78 | if self.ka_task: 79 | self.ka_task.cancel() 80 | self.ka_task = None 81 | 82 | 83 | async def connect(self, server_info, proto_code=None, *, 84 | use_tor=False, disable_cert_verify=False, 85 | proxy=None, short_term=False, disconnect_callback=None): 86 | ''' 87 | Start connection process. 88 | Destination must be specified in a ServerInfo() record (first arg). 89 | ''' 90 | self.server_info = server_info 91 | self.disconnect_callback = disconnect_callback 92 | if not proto_code: 93 | proto_code,*_ = server_info.protocols 94 | self.proto_code = proto_code 95 | 96 | logger.debug("Connecting to: %r" % server_info) 97 | 98 | if proto_code == 'g': # websocket 99 | # to do this, we'll need a websockets implementation that 100 | # operates more like a asyncio.Transport 101 | # maybe: `asyncws` or `aiohttp` 102 | raise NotImplementedError('sorry no WebSocket transport yet') 103 | 104 | hostname, port, use_ssl = server_info.get_port(proto_code) 105 | 106 | if use_tor: 107 | if have_aiosocks: 108 | # Connect via Tor proxy proxy, assumed to be on localhost:9050 109 | # unless a tuple is given with another host/port combo. 110 | try: 111 | socks_host, socks_port = use_tor 112 | except TypeError: 113 | socks_host, socks_port = 'localhost', 9050 114 | 115 | # basically no-one has .onion SSL certificates, and 116 | # pointless anyway. 117 | disable_cert_verify = True 118 | 119 | assert not proxy, "Sorry not yet supporting proxy->tor->dest" 120 | 121 | logger.debug(" .. using TOR") 122 | 123 | proxy = aiosocks.Socks5Addr(socks_host, int(socks_port)) 124 | else: 125 | logger.debug("Error: want to use tor, but no aiosocks module.") 126 | 127 | if use_ssl == True and disable_cert_verify: 128 | # Create a more liberal SSL context that won't 129 | # object to self-signed certicates. This is 130 | # very bad on public Internet, but probably ok 131 | # over Tor 132 | use_ssl = ssl.create_default_context() 133 | use_ssl.check_hostname = False 134 | use_ssl.verify_mode = ssl.CERT_NONE 135 | 136 | logger.debug(" .. SSL cert check disabled") 137 | 138 | async def _reconnect(): 139 | if self.protocol: return # race/duplicate work 140 | 141 | if proxy: 142 | if have_aiosocks: 143 | transport, protocol = await aiosocks.create_connection( 144 | StratumProtocol, proxy=proxy, 145 | proxy_auth=None, 146 | remote_resolve=True, ssl=use_ssl, 147 | dst=(hostname, port)) 148 | else: 149 | logger.debug("Error: want to use proxy, but no aiosocks module.") 150 | else: 151 | transport, protocol = await self.loop.create_connection( 152 | StratumProtocol, host=hostname, 153 | port=port, ssl=use_ssl) 154 | 155 | self.protocol = protocol 156 | protocol.client = self 157 | 158 | # capture actual values used 159 | self.actual_connection = dict(hostname=hostname, port=int(port), 160 | ssl=bool(use_ssl), tor=bool(proxy)) 161 | self.actual_connection['ip_addr'] = transport.get_extra_info('peername', 162 | default=['unknown'])[0] 163 | 164 | # always report our version, and get server's version 165 | await self.get_server_version() 166 | logger.debug(f"Server version/protocol: {self.server_version} / {self.protocol_version}") 167 | 168 | if not short_term: 169 | self.ka_task = self.loop.create_task(self._keepalive()) 170 | 171 | logger.debug("Connected to: %r" % server_info) 172 | 173 | # close whatever we had 174 | if self.protocol: 175 | self.protocol.close() 176 | self.protocol = None 177 | 178 | self.reconnect = _reconnect 179 | await self.reconnect() 180 | 181 | async def get_server_version(self): 182 | # fetch version strings, save them 183 | # - can only be done once in v1.4 184 | self.server_version, pv = await self.RPC('server.version', *self.my_version_args) 185 | self.protocol_version = float(pv) 186 | 187 | async def _keepalive(self): 188 | ''' 189 | Keep our connect to server alive forever, with some 190 | pointless traffic. 191 | ''' 192 | while self.protocol: 193 | await self.RPC('server.ping') 194 | 195 | # Docs now say "The server may disconnect clients that have sent 196 | # no requests for roughly 10 minutes" ... so use 5 minutes here 197 | await asyncio.sleep(5*60) 198 | 199 | 200 | def _send_request(self, method, params=[], is_subscribe = False): 201 | ''' 202 | Send a new request to the server. Serialized the JSON and 203 | tracks id numbers and optional callbacks. 204 | ''' 205 | 206 | if method.startswith('blockchain.address.'): 207 | # these methods have changed, but we can patch them 208 | method, params = self.patch_addr_methods(method, params) 209 | 210 | # pick a new ID 211 | self.next_id += 1 212 | req_id = self.next_id 213 | 214 | # serialize as JSON 215 | msg = {'id': req_id, 'method': method, 'params': params} 216 | 217 | # subscriptions are a Q, normal requests are a future 218 | if is_subscribe: 219 | waitQ = asyncio.Queue() 220 | self.subscriptions[method].append(waitQ) 221 | 222 | fut = asyncio.Future(loop=self.loop) 223 | 224 | self.inflight[req_id] = (msg, fut) 225 | 226 | logger.debug(" REQ: %r" % msg) 227 | 228 | # send it via the transport, which serializes it 229 | if not self.protocol: 230 | logger.debug("Need to reconnect to server") 231 | 232 | async def connect_first(): 233 | await self.reconnect() 234 | self.protocol.send_data(msg) 235 | 236 | self.loop.create_task(connect_first()) 237 | else: 238 | # typical case, send request immediatedly, response is a future 239 | self.protocol.send_data(msg) 240 | 241 | return fut if not is_subscribe else (fut, waitQ) 242 | 243 | def _send_batch_requests(self, requests): 244 | ''' 245 | Send a new batch of requests to the server. 246 | ''' 247 | 248 | full_msg = [] 249 | 250 | for method, *params in requests: 251 | 252 | if method.startswith('blockchain.address.'): 253 | # these methods have changed, but we can patch them 254 | method, params = self.patch_addr_methods(method, params) 255 | 256 | # pick a new ID 257 | self.next_id += 1 258 | req_id = self.next_id 259 | 260 | # serialize as JSON 261 | msg = {'id': req_id, 'method': method, 'params': params} 262 | 263 | full_msg.append(msg) 264 | 265 | fut = asyncio.Future(loop=self.loop) 266 | first_msg = full_msg[0] 267 | 268 | self.inflight[first_msg['id']] = (full_msg, fut) 269 | 270 | logger.debug(" REQ: %r" % full_msg) 271 | 272 | # send it via the transport, which serializes it 273 | if not self.protocol: 274 | logger.debug("Need to reconnect to server") 275 | 276 | async def connect_first(): 277 | await self.reconnect() 278 | self.protocol.send_data(full_msg) 279 | 280 | self.loop.create_task(connect_first()) 281 | else: 282 | # typical case, send request immediately, response is a future 283 | self.protocol.send_data(full_msg) 284 | 285 | return fut 286 | 287 | def _got_response(self, msg): 288 | ''' 289 | Decode and dispatch responses from the server. 290 | 291 | Has already been unframed and deserialized into an object. 292 | ''' 293 | 294 | logger.debug("RESP: %r" % msg) 295 | 296 | if isinstance(msg, list): 297 | # we are dealing with a batch request 298 | 299 | inf = None 300 | for response in msg: 301 | resp_id = response.get('id', None) 302 | inf = self.inflight.pop(resp_id, None) 303 | if inf: 304 | break 305 | 306 | if not inf: 307 | first_msg = msg[0] 308 | logger.error("Incoming server message had unknown ID in it: %s" % first_msg['id']) 309 | return 310 | 311 | # it's a future which is done now 312 | full_req, rv = inf 313 | 314 | response_map = {resp['id']: resp for resp in msg} 315 | results = [] 316 | for request in full_req: 317 | req_id = request.get('id', None) 318 | 319 | response = response_map.get(req_id, None) 320 | if not response: 321 | logger.error("Incoming server message had missing ID: %s" % req_id) 322 | 323 | error = response.get('error', None) 324 | if error: 325 | logger.info("Error response: '%s'" % error) 326 | rv.set_exception(ElectrumErrorResponse(error, request)) 327 | 328 | result = response.get('result') 329 | results.append(result) 330 | 331 | rv.set_result(results) 332 | return 333 | 334 | resp_id = msg.get('id', None) 335 | 336 | if resp_id is None: 337 | # subscription traffic comes with method set, but no req id. 338 | method = msg.get('method', None) 339 | if not method: 340 | logger.error("Incoming server message had no ID nor method in it", msg) 341 | return 342 | 343 | # not obvious, but result is on params, not result, for subscriptions 344 | result = msg.get('params', None) 345 | 346 | logger.debug("Traffic on subscription: %s" % method) 347 | 348 | subs = self.subscriptions.get(method) 349 | for q in subs: 350 | self.loop.create_task(q.put(result)) 351 | 352 | return 353 | 354 | assert 'method' not in msg 355 | result = msg.get('result') 356 | 357 | # fetch and forget about the request 358 | inf = self.inflight.pop(resp_id) 359 | if not inf: 360 | logger.error("Incoming server message had unknown ID in it: %s" % resp_id) 361 | return 362 | 363 | # it's a future which is done now 364 | req, rv = inf 365 | 366 | if 'error' in msg: 367 | err = msg['error'] 368 | 369 | logger.info("Error response: '%s'" % err) 370 | rv.set_exception(ElectrumErrorResponse(err, req)) 371 | 372 | else: 373 | rv.set_result(result) 374 | 375 | def RPC(self, method, *params): 376 | ''' 377 | Perform a remote command. 378 | 379 | Expects a method name, which look like: 380 | 381 | blockchain.address.get_balance 382 | 383 | .. and sometimes take arguments, all of which are positional. 384 | 385 | Returns a future which will you should await for 386 | the result from the server. Failures are returned as exceptions. 387 | ''' 388 | assert '.' in method 389 | #assert not method.endswith('subscribe') 390 | 391 | return self._send_request(method, params) 392 | 393 | def batch_rpc(self, requests): 394 | ''' 395 | Perform a batch of remote commands. 396 | 397 | Expects a list of ("method name", params...) tuples, where the method name should look 398 | like: 399 | 400 | blockchain.address.get_balance 401 | 402 | .. and sometimes take arguments, all of which are positional. 403 | 404 | Returns a future which will you should await for the list of results for each command 405 | from the server. Failures are returned as exceptions. 406 | ''' 407 | for request in requests: 408 | assert isinstance(request, tuple) 409 | method, *params = request 410 | assert '.' in method 411 | 412 | return self._send_batch_requests(requests) 413 | 414 | def patch_addr_methods(self, method, params): 415 | # blockchain.address.get_balance(addr) => blockchain.scripthash.get_balance(sh) 416 | from hashlib import sha256 417 | from binascii import b2a_hex 418 | try: 419 | from pycoin.symbols.btc import network as BTC # bitcoin only! 420 | except ImportError: 421 | raise RuntimeError("we can patch obsolete protocol msgs, but need pycoin>=0.90") 422 | 423 | # convert from base58 into sha256(binary of script)? 424 | addr = BTC.parse(params[0]) 425 | sh = sha256(addr.script()).digest()[::-1] 426 | 427 | return method.replace('.address.', '.scripthash.'), \ 428 | [str(b2a_hex(sh), 'ascii')]+list(params[1:]) 429 | 430 | def subscribe(self, method, *params): 431 | ''' 432 | Perform a remote command which will stream events/data to us. 433 | 434 | Expects a method name, which look like: 435 | server.peers.subscribe 436 | .. and sometimes take arguments, all of which are positional. 437 | 438 | Returns a tuple: (Future, asyncio.Queue). 439 | The future will have the result of the initial 440 | call, and the queue will receive additional 441 | responses as they happen. 442 | ''' 443 | assert '.' in method 444 | assert method.endswith('subscribe') 445 | return self._send_request(method, params, is_subscribe=True) 446 | 447 | 448 | if __name__ == '__main__': 449 | from transport import SocketTransport 450 | from svr_info import KnownServers, ServerInfo 451 | 452 | logging.basicConfig(format="%(asctime)-11s %(message)s", datefmt="[%d/%m/%Y-%H:%M:%S]") 453 | 454 | loop = asyncio.get_event_loop() 455 | loop.set_debug(True) 456 | 457 | proto_code = 's' 458 | 459 | if 0: 460 | ks = KnownServers() 461 | ks.from_json('servers.json') 462 | which = ks.select(proto_code, is_onion=True, min_prune=1000)[0] 463 | else: 464 | which = ServerInfo({ 465 | "seen_at": 1465686119.022801, 466 | "ports": "t s", 467 | "nickname": "dunp", 468 | "pruning_limit": 10000, 469 | "version": "1.0", 470 | "hostname": "erbium1.sytes.net" }) 471 | 472 | c = StratumClient(loop=loop) 473 | 474 | loop.run_until_complete(c.connect(which, proto_code, disable_cert_verify=True, use_tor=True)) 475 | 476 | rv = loop.run_until_complete(c.RPC('server.peers.subscribe')) 477 | print("DONE!: this server has %d peers" % len(rv)) 478 | loop.close() 479 | 480 | #c.blockchain.address.get_balance(23) 481 | -------------------------------------------------------------------------------- /connectrum/servers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "nickname": null, 4 | "hostname": "104.250.141.242", 5 | "ip_addr": null, 6 | "ports": [ 7 | "s50002" 8 | ], 9 | "version": "1.1", 10 | "pruning_limit": 0, 11 | "seen_at": 1533670768.8676639 12 | }, 13 | { 14 | "nickname": null, 15 | "hostname": "13.80.67.162", 16 | "ip_addr": null, 17 | "ports": [ 18 | "s50002", 19 | "t50001" 20 | ], 21 | "version": "1.2", 22 | "pruning_limit": 0, 23 | "seen_at": 1533670768.8675451 24 | }, 25 | { 26 | "nickname": null, 27 | "hostname": "134.119.179.55", 28 | "ip_addr": null, 29 | "ports": [ 30 | "s50002" 31 | ], 32 | "version": "1.0", 33 | "pruning_limit": 0, 34 | "seen_at": 1533670768.731586 35 | }, 36 | { 37 | "nickname": null, 38 | "hostname": "139.162.14.142", 39 | "ip_addr": null, 40 | "ports": [ 41 | "s50002", 42 | "t50001" 43 | ], 44 | "version": "1.1", 45 | "pruning_limit": 0, 46 | "seen_at": 1533670768.8676212 47 | }, 48 | { 49 | "nickname": null, 50 | "hostname": "165.227.202.193", 51 | "ip_addr": null, 52 | "ports": [ 53 | "s50002", 54 | "t50001" 55 | ], 56 | "version": "1.2", 57 | "pruning_limit": 0, 58 | "seen_at": 1533670768.584981 59 | }, 60 | { 61 | "nickname": null, 62 | "hostname": "165.227.22.180", 63 | "ip_addr": null, 64 | "ports": [ 65 | "s50002", 66 | "t50001" 67 | ], 68 | "version": "1.1", 69 | "pruning_limit": 0, 70 | "seen_at": 1533670768.867455 71 | }, 72 | { 73 | "nickname": null, 74 | "hostname": "185.64.116.15", 75 | "ip_addr": null, 76 | "ports": [ 77 | "s50002", 78 | "t50001" 79 | ], 80 | "version": "1.2", 81 | "pruning_limit": 0, 82 | "seen_at": 1533670768.8675652 83 | }, 84 | { 85 | "nickname": null, 86 | "hostname": "207.154.223.80", 87 | "ip_addr": null, 88 | "ports": [ 89 | "s50002", 90 | "t50001" 91 | ], 92 | "version": "1.2", 93 | "pruning_limit": 0, 94 | "seen_at": 1533670768.867737 95 | }, 96 | { 97 | "nickname": null, 98 | "hostname": "3smoooajg7qqac2y.onion", 99 | "ip_addr": null, 100 | "ports": [ 101 | "s50002", 102 | "t50001" 103 | ], 104 | "version": "1.4", 105 | "pruning_limit": 0, 106 | "seen_at": 1533670768.867671 107 | }, 108 | { 109 | "nickname": null, 110 | "hostname": "3tm3fjg3ds5fcibw.onion", 111 | "ip_addr": null, 112 | "ports": [ 113 | "t50001" 114 | ], 115 | "version": "1.4", 116 | "pruning_limit": 0, 117 | "seen_at": 1533670768.867779 118 | }, 119 | { 120 | "nickname": "electroli", 121 | "hostname": "46.166.165.18", 122 | "ip_addr": null, 123 | "ports": [ 124 | "t", 125 | "s" 126 | ], 127 | "version": "1.0", 128 | "pruning_limit": 10000, 129 | "seen_at": 1465686119.945237 130 | }, 131 | { 132 | "nickname": null, 133 | "hostname": "4cii7ryno5j3axe4.onion", 134 | "ip_addr": null, 135 | "ports": [ 136 | "t50001" 137 | ], 138 | "version": "1.2", 139 | "pruning_limit": 0, 140 | "seen_at": 1533670768.86764 141 | }, 142 | { 143 | "nickname": null, 144 | "hostname": "4yi77lkjgy4bwtj3.onion", 145 | "ip_addr": null, 146 | "ports": [ 147 | "s50002", 148 | "t50001" 149 | ], 150 | "version": "1.1", 151 | "pruning_limit": 0, 152 | "seen_at": 1533670768.86769 153 | }, 154 | { 155 | "nickname": null, 156 | "hostname": "7jwtirwsaogb6jv2.onion", 157 | "ip_addr": null, 158 | "ports": [ 159 | "s50002", 160 | "t50001" 161 | ], 162 | "version": "1.2", 163 | "pruning_limit": 0, 164 | "seen_at": 1533670768.8675048 165 | }, 166 | { 167 | "nickname": null, 168 | "hostname": "abc1.hsmiths.com", 169 | "ip_addr": "76.174.26.91", 170 | "ports": [ 171 | "s60002", 172 | "t60001" 173 | ], 174 | "version": "1.4", 175 | "pruning_limit": 0, 176 | "seen_at": 1533670768.584984 177 | }, 178 | { 179 | "nickname": null, 180 | "hostname": "alviss.coinjoined.com", 181 | "ip_addr": "94.130.136.185", 182 | "ports": [ 183 | "s50002", 184 | "t50001" 185 | ], 186 | "version": "1.1", 187 | "pruning_limit": 0, 188 | "seen_at": 1533670768.867656 189 | }, 190 | { 191 | "nickname": "anducknetelect", 192 | "hostname": "anduck.net", 193 | "ip_addr": null, 194 | "ports": [ 195 | "t", 196 | "s" 197 | ], 198 | "version": "1.0", 199 | "pruning_limit": 100, 200 | "seen_at": 1465686119.022705 201 | }, 202 | { 203 | "nickname": "antumbra", 204 | "hostname": "antumbra.se", 205 | "ip_addr": null, 206 | "ports": [ 207 | "t", 208 | "s" 209 | ], 210 | "version": "1.0", 211 | "pruning_limit": 10000, 212 | "seen_at": 1465686119.022753 213 | }, 214 | { 215 | "nickname": "j_fdk_b", 216 | "hostname": "b.1209k.com", 217 | "ip_addr": null, 218 | "ports": [ 219 | "t", 220 | "s" 221 | ], 222 | "version": "1.0", 223 | "pruning_limit": 10000, 224 | "seen_at": 1465686119.020474 225 | }, 226 | { 227 | "nickname": null, 228 | "hostname": "bauerjda5hnedjam.onion", 229 | "ip_addr": null, 230 | "ports": [ 231 | "s50002", 232 | "t50001" 233 | ], 234 | "version": "1.4", 235 | "pruning_limit": 0, 236 | "seen_at": 1533670768.867549 237 | }, 238 | { 239 | "nickname": null, 240 | "hostname": "bch.curalle.ovh", 241 | "ip_addr": "176.31.100.47", 242 | "ports": [ 243 | "s50002" 244 | ], 245 | "version": "1.4", 246 | "pruning_limit": 0, 247 | "seen_at": 1533670768.584946 248 | }, 249 | { 250 | "nickname": null, 251 | "hostname": "bch.electrumx.cash", 252 | "ip_addr": "2a01:4f8:160:31a6::2", 253 | "ports": [ 254 | "s50002", 255 | "t50001" 256 | ], 257 | "version": "1.4", 258 | "pruning_limit": 0, 259 | "seen_at": 1533670768.5849369 260 | }, 261 | { 262 | "nickname": null, 263 | "hostname": "bch.imaginary.cash", 264 | "ip_addr": "198.27.66.168", 265 | "ports": [ 266 | "s50002", 267 | "t50001" 268 | ], 269 | "version": "1.4", 270 | "pruning_limit": 0, 271 | "seen_at": 1533670768.584942 272 | }, 273 | { 274 | "nickname": null, 275 | "hostname": "bch.stitthappens.com", 276 | "ip_addr": "157.131.220.238", 277 | "ports": [ 278 | "s50002", 279 | "t50001" 280 | ], 281 | "version": "1.4", 282 | "pruning_limit": 0, 283 | "seen_at": 1533670768.58495 284 | }, 285 | { 286 | "nickname": null, 287 | "hostname": "bch0.kister.net", 288 | "ip_addr": "98.115.251.214", 289 | "ports": [ 290 | "s50002", 291 | "t50001" 292 | ], 293 | "version": "1.2", 294 | "pruning_limit": 0, 295 | "seen_at": 1533670768.5849988 296 | }, 297 | { 298 | "nickname": null, 299 | "hostname": "Bitcoin-node.nl", 300 | "ip_addr": "82.217.214.215", 301 | "ports": [ 302 | "s50002", 303 | "t50001" 304 | ], 305 | "version": "1.1", 306 | "pruning_limit": 0, 307 | "seen_at": 1533670768.867786 308 | }, 309 | { 310 | "nickname": null, 311 | "hostname": "bitcoin.cluelessperson.com", 312 | "ip_addr": "172.92.140.254", 313 | "ports": [ 314 | "s50002", 315 | "t50001" 316 | ], 317 | "version": "1.2", 318 | "pruning_limit": 0, 319 | "seen_at": 1533670768.588772 320 | }, 321 | { 322 | "nickname": null, 323 | "hostname": "bitcoin.corgi.party", 324 | "ip_addr": "176.223.139.65", 325 | "ports": [ 326 | "s50002", 327 | "t50001" 328 | ], 329 | "version": "1.4", 330 | "pruning_limit": 0, 331 | "seen_at": 1533670768.867529 332 | }, 333 | { 334 | "nickname": null, 335 | "hostname": "bitcoin.grey.pw", 336 | "ip_addr": "173.249.8.197", 337 | "ports": [ 338 | "s50002", 339 | "t50001" 340 | ], 341 | "version": "1.4", 342 | "pruning_limit": 0, 343 | "seen_at": 1533670768.8676748 344 | }, 345 | { 346 | "nickname": null, 347 | "hostname": "bitcoin3nqy3db7c.onion", 348 | "ip_addr": null, 349 | "ports": [ 350 | "s50002", 351 | "t50001" 352 | ], 353 | "version": "1.4", 354 | "pruning_limit": 0, 355 | "seen_at": 1533670768.867647 356 | }, 357 | { 358 | "nickname": null, 359 | "hostname": "bitcoincash.quangld.com", 360 | "ip_addr": "14.161.3.136", 361 | "ports": [ 362 | "s50002", 363 | "t50001" 364 | ], 365 | "version": "1.2", 366 | "pruning_limit": 0, 367 | "seen_at": 1533670768.5849721 368 | }, 369 | { 370 | "nickname": null, 371 | "hostname": "bitcoins.sk", 372 | "ip_addr": "46.229.238.187", 373 | "ports": [ 374 | "s50002", 375 | "t50001" 376 | ], 377 | "version": "1.2", 378 | "pruning_limit": 0, 379 | "seen_at": 1533670768.867497 380 | }, 381 | { 382 | "nickname": null, 383 | "hostname": "bitnode.pw", 384 | "ip_addr": "81.183.169.117", 385 | "ports": [ 386 | "s57081" 387 | ], 388 | "version": "1.4", 389 | "pruning_limit": 0, 390 | "seen_at": 1533670768.731807 391 | }, 392 | { 393 | "nickname": null, 394 | "hostname": "btc.cihar.com", 395 | "ip_addr": "78.46.177.74", 396 | "ports": [ 397 | "s50002", 398 | "t50001" 399 | ], 400 | "version": "1.4", 401 | "pruning_limit": 0, 402 | "seen_at": 1533670768.759933 403 | }, 404 | { 405 | "nickname": null, 406 | "hostname": "btc.gravitech.net", 407 | "ip_addr": "37.187.167.132", 408 | "ports": [ 409 | "s50002" 410 | ], 411 | "version": "1.4", 412 | "pruning_limit": 0, 413 | "seen_at": 1533670768.86759 414 | }, 415 | { 416 | "nickname": "mustyoshi", 417 | "hostname": "btc.mustyoshi.com", 418 | "ip_addr": null, 419 | "ports": [ 420 | "t", 421 | "s" 422 | ], 423 | "version": "1.0", 424 | "pruning_limit": 10000, 425 | "seen_at": 1465686119.945437 426 | }, 427 | { 428 | "nickname": null, 429 | "hostname": "btc.outoftime.co", 430 | "ip_addr": "121.44.121.158", 431 | "ports": [ 432 | "s50002", 433 | "t50001" 434 | ], 435 | "version": "1.4", 436 | "pruning_limit": 0, 437 | "seen_at": 1533670768.867712 438 | }, 439 | { 440 | "nickname": null, 441 | "hostname": "btc.pr0xima.de", 442 | "ip_addr": "5.189.191.123", 443 | "ports": [ 444 | "s50002", 445 | "t50001" 446 | ], 447 | "version": "1.2", 448 | "pruning_limit": 0, 449 | "seen_at": 1533670768.867445 450 | }, 451 | { 452 | "nickname": "selavi", 453 | "hostname": "btc.smsys.me", 454 | "ip_addr": null, 455 | "ports": [ 456 | "t110", 457 | "s995" 458 | ], 459 | "version": "1.0", 460 | "pruning_limit": 10000, 461 | "seen_at": 1465686119.021612 462 | }, 463 | { 464 | "nickname": null, 465 | "hostname": "btc.xskyx.net", 466 | "ip_addr": "185.183.158.170", 467 | "ports": [ 468 | "s50002", 469 | "t50001" 470 | ], 471 | "version": "1.2", 472 | "pruning_limit": 0, 473 | "seen_at": 1533670768.867489 474 | }, 475 | { 476 | "nickname": "cplus", 477 | "hostname": "btc1.commerce-plus.com", 478 | "ip_addr": null, 479 | "ports": [ 480 | "t", 481 | "s" 482 | ], 483 | "version": "1.0", 484 | "pruning_limit": 10000, 485 | "seen_at": 1465686119.96599 486 | }, 487 | { 488 | "nickname": "clueless", 489 | "hostname": "cluelessperson.com", 490 | "ip_addr": null, 491 | "ports": [ 492 | "t", 493 | "s" 494 | ], 495 | "version": "1.0", 496 | "pruning_limit": 10000, 497 | "seen_at": 1465686119.021714 498 | }, 499 | { 500 | "nickname": "condor1003", 501 | "hostname": "condor1003.server4you.de", 502 | "ip_addr": null, 503 | "ports": [ 504 | "t", 505 | "s" 506 | ], 507 | "version": "1.0", 508 | "pruning_limit": 10000, 509 | "seen_at": 1465686119.945338 510 | }, 511 | { 512 | "nickname": null, 513 | "hostname": "crypto.mldlabs.com", 514 | "ip_addr": "209.160.27.54", 515 | "ports": [ 516 | "s50002", 517 | "t50001" 518 | ], 519 | "version": "1.2", 520 | "pruning_limit": 0, 521 | "seen_at": 1533670768.584996 522 | }, 523 | { 524 | "nickname": null, 525 | "hostname": "currentlane.lovebitco.in", 526 | "ip_addr": "88.198.91.74", 527 | "ports": [ 528 | "s50002", 529 | "t50001" 530 | ], 531 | "version": "1.4", 532 | "pruning_limit": 0, 533 | "seen_at": 1533670768.8676338 534 | }, 535 | { 536 | "nickname": null, 537 | "hostname": "daedalus.bauerj.eu", 538 | "ip_addr": "84.200.105.74", 539 | "ports": [ 540 | "s50002", 541 | "t50001" 542 | ], 543 | "version": "1.4", 544 | "pruning_limit": 0, 545 | "seen_at": 1533670768.8677042 546 | }, 547 | { 548 | "nickname": null, 549 | "hostname": "dxm.no-ip.biz", 550 | "ip_addr": "78.51.123.122", 551 | "ports": [ 552 | "s50002", 553 | "t50001" 554 | ], 555 | "version": "1.4", 556 | "pruning_limit": 0, 557 | "seen_at": 1533670768.628011 558 | }, 559 | { 560 | "nickname": null, 561 | "hostname": "e-1.claudioboxx.com", 562 | "ip_addr": "37.61.209.146", 563 | "ports": [ 564 | "s50002", 565 | "t50001" 566 | ], 567 | "version": "1.4", 568 | "pruning_limit": 0, 569 | "seen_at": 1533670768.8677719 570 | }, 571 | { 572 | "nickname": null, 573 | "hostname": "e-2.claudioboxx.com", 574 | "ip_addr": "37.61.209.147", 575 | "ports": [ 576 | "s50002", 577 | "t50001" 578 | ], 579 | "version": "1.4", 580 | "pruning_limit": 0, 581 | "seen_at": 1533670768.867769 582 | }, 583 | { 584 | "nickname": null, 585 | "hostname": "e-4.claudioboxx.com", 586 | "ip_addr": "134.119.179.67", 587 | "ports": [ 588 | "s50002", 589 | "t50001" 590 | ], 591 | "version": "1.4", 592 | "pruning_limit": 0, 593 | "seen_at": 1533670768.8678012 594 | }, 595 | { 596 | "nickname": null, 597 | "hostname": "E-X.not.fyi", 598 | "ip_addr": "170.130.28.174", 599 | "ports": [ 600 | "s50002", 601 | "t50001" 602 | ], 603 | "version": "1.4", 604 | "pruning_limit": 0, 605 | "seen_at": 1533670768.86766 606 | }, 607 | { 608 | "nickname": null, 609 | "hostname": "e.keff.org", 610 | "ip_addr": "194.71.109.91", 611 | "ports": [ 612 | "s50002", 613 | "t50001" 614 | ], 615 | "version": "1.2", 616 | "pruning_limit": 0, 617 | "seen_at": 1533670768.867472 618 | }, 619 | { 620 | "nickname": "ECDSA", 621 | "hostname": "ecdsa.net", 622 | "ip_addr": null, 623 | "ports": [ 624 | "t", 625 | "s110" 626 | ], 627 | "version": "1.0", 628 | "pruning_limit": 100, 629 | "seen_at": 1465686119.02029 630 | }, 631 | { 632 | "nickname": "fydel", 633 | "hostname": "ele.einfachmalnettsein.de", 634 | "ip_addr": null, 635 | "ports": [ 636 | "t", 637 | "s" 638 | ], 639 | "version": "1.0", 640 | "pruning_limit": 10000, 641 | "seen_at": 1465686119.022509 642 | }, 643 | { 644 | "nickname": "Luggs", 645 | "hostname": "elec.luggs.co", 646 | "ip_addr": "95.211.185.14", 647 | "ports": [ 648 | "s443" 649 | ], 650 | "version": "1.4", 651 | "pruning_limit": 0, 652 | "seen_at": 1533670768.867761 653 | }, 654 | { 655 | "nickname": "Pielectrum", 656 | "hostname": "ELEC.Pieh0.co.uk", 657 | "ip_addr": null, 658 | "ports": [ 659 | "t", 660 | "s" 661 | ], 662 | "version": "1.0", 663 | "pruning_limit": 10000, 664 | "seen_at": 1465686119.022244 665 | }, 666 | { 667 | "nickname": null, 668 | "hostname": "electron-cash.dragon.zone", 669 | "ip_addr": "2607:5300:60:84ec::70", 670 | "ports": [ 671 | "s50002", 672 | "t50001" 673 | ], 674 | "version": "1.2", 675 | "pruning_limit": 0, 676 | "seen_at": 1533670768.5849879 677 | }, 678 | { 679 | "nickname": null, 680 | "hostname": "electron.coinucopia.io", 681 | "ip_addr": "138.197.193.154", 682 | "ports": [ 683 | "s50002", 684 | "t50001" 685 | ], 686 | "version": "1.2", 687 | "pruning_limit": 0, 688 | "seen_at": 1533670768.584977 689 | }, 690 | { 691 | "nickname": null, 692 | "hostname": "electroncash.bitcoinplug.com", 693 | "ip_addr": "138.197.204.148", 694 | "ports": [ 695 | "s50002", 696 | "t50001" 697 | ], 698 | "version": "1.2", 699 | "pruning_limit": 0, 700 | "seen_at": 1533670768.584954 701 | }, 702 | { 703 | "nickname": null, 704 | "hostname": "electroncash.cascharia.com", 705 | "ip_addr": "78.46.67.111", 706 | "ports": [ 707 | "s50002", 708 | "t50001" 709 | ], 710 | "version": "1.4", 711 | "pruning_limit": 0, 712 | "seen_at": 1533670768.5849311 713 | }, 714 | { 715 | "nickname": null, 716 | "hostname": "electroncash.dk", 717 | "ip_addr": "2001:470:de5a::ec", 718 | "ports": [ 719 | "s50002", 720 | "t50001" 721 | ], 722 | "version": "1.4", 723 | "pruning_limit": 0, 724 | "seen_at": 1533670768.584959 725 | }, 726 | { 727 | "nickname": null, 728 | "hostname": "electroncash.ueo.ch", 729 | "ip_addr": "5.9.70.44", 730 | "ports": [ 731 | "s51002", 732 | "t51001" 733 | ], 734 | "version": "1.2", 735 | "pruning_limit": 0, 736 | "seen_at": 1533670768.585011 737 | }, 738 | { 739 | "nickname": "trouth_eu", 740 | "hostname": "electrum-europe.trouth.net", 741 | "ip_addr": null, 742 | "ports": [ 743 | "t", 744 | "s" 745 | ], 746 | "version": "1.0", 747 | "pruning_limit": 10000, 748 | "seen_at": 1465686119.040206 749 | }, 750 | { 751 | "nickname": null, 752 | "hostname": "electrum-server.ninja", 753 | "ip_addr": "220.233.178.199", 754 | "ports": [ 755 | "s50002", 756 | "t50001" 757 | ], 758 | "version": "1.4", 759 | "pruning_limit": 0, 760 | "seen_at": 1533670768.867697 761 | }, 762 | { 763 | "nickname": null, 764 | "hostname": "electrum-unlimited.criptolayer.net", 765 | "ip_addr": "188.40.93.205", 766 | "ports": [ 767 | "s50002" 768 | ], 769 | "version": "1.4", 770 | "pruning_limit": 0, 771 | "seen_at": 1533670768.696258 772 | }, 773 | { 774 | "nickname": "molec", 775 | "hostname": "electrum.0x0000.de", 776 | "ip_addr": null, 777 | "ports": [ 778 | "t", 779 | "s" 780 | ], 781 | "version": "1.0", 782 | "pruning_limit": 10000, 783 | "seen_at": 1465686119.966419 784 | }, 785 | { 786 | "nickname": null, 787 | "hostname": "electrum.anduck.net", 788 | "ip_addr": "62.210.6.26", 789 | "ports": [ 790 | "s50002", 791 | "t50001" 792 | ], 793 | "version": "1.2", 794 | "pruning_limit": 0, 795 | "seen_at": 1533670768.86758 796 | }, 797 | { 798 | "nickname": "anonymized1", 799 | "hostname": "electrum.anonymized.io", 800 | "ip_addr": null, 801 | "ports": [ 802 | "t", 803 | "s" 804 | ], 805 | "version": "1.0", 806 | "pruning_limit": 10000, 807 | "seen_at": 1465686119.020339 808 | }, 809 | { 810 | "nickname": null, 811 | "hostname": "electrum.be", 812 | "ip_addr": "88.198.241.196", 813 | "ports": [ 814 | "s50002", 815 | "t50001" 816 | ], 817 | "version": "1.2", 818 | "pruning_limit": 0, 819 | "seen_at": 1533670768.867438 820 | }, 821 | { 822 | "nickname": null, 823 | "hostname": "electrum.coineuskal.com", 824 | "ip_addr": "34.207.56.59", 825 | "ports": [ 826 | "s50002", 827 | "t50001" 828 | ], 829 | "version": "1.2", 830 | "pruning_limit": 0, 831 | "seen_at": 1533670768.8676531 832 | }, 833 | { 834 | "nickname": null, 835 | "hostname": "electrum.coinucopia.io", 836 | "ip_addr": "67.205.187.44", 837 | "ports": [ 838 | "s50002", 839 | "t50001" 840 | ], 841 | "version": "1.2", 842 | "pruning_limit": 0, 843 | "seen_at": 1533670768.867501 844 | }, 845 | { 846 | "nickname": null, 847 | "hostname": "electrum.dk", 848 | "ip_addr": "92.246.24.225", 849 | "ports": [ 850 | "s50002", 851 | "t50001" 852 | ], 853 | "version": "1.1", 854 | "pruning_limit": 0, 855 | "seen_at": 1533670768.867573 856 | }, 857 | { 858 | "nickname": "DragonZone", 859 | "hostname": "electrum.dragonzone.net", 860 | "ip_addr": null, 861 | "ports": [ 862 | "t", 863 | "h", 864 | "s", 865 | "g" 866 | ], 867 | "version": "1.0", 868 | "pruning_limit": 10000, 869 | "seen_at": 1465686119.966315 870 | }, 871 | { 872 | "nickname": null, 873 | "hostname": "electrum.eff.ro", 874 | "ip_addr": "185.36.252.200", 875 | "ports": [ 876 | "s50002", 877 | "t50001" 878 | ], 879 | "version": "1.4", 880 | "pruning_limit": 0, 881 | "seen_at": 1533670768.867525 882 | }, 883 | { 884 | "nickname": null, 885 | "hostname": "electrum.festivaldelhumor.org", 886 | "ip_addr": "173.212.247.250", 887 | "ports": [ 888 | "s50002", 889 | "t50001" 890 | ], 891 | "version": "1.4", 892 | "pruning_limit": 0, 893 | "seen_at": 1533670768.8674629 894 | }, 895 | { 896 | "nickname": "hsmiths", 897 | "hostname": "electrum.hsmiths.com", 898 | "ip_addr": "76.174.26.91", 899 | "ports": [ 900 | "s50002", 901 | "t50001" 902 | ], 903 | "version": "1.4", 904 | "pruning_limit": 0, 905 | "seen_at": 1533670768.867747 906 | }, 907 | { 908 | "nickname": null, 909 | "hostname": "electrum.imaginary.cash", 910 | "ip_addr": "193.29.187.78", 911 | "ports": [ 912 | "s50002", 913 | "t50001" 914 | ], 915 | "version": "1.4", 916 | "pruning_limit": 0, 917 | "seen_at": 1533670768.585015 918 | }, 919 | { 920 | "nickname": null, 921 | "hostname": "electrum.infinitum-nihil.com", 922 | "ip_addr": "192.30.120.110", 923 | "ports": [ 924 | "s50002" 925 | ], 926 | "version": "1.0", 927 | "pruning_limit": 0, 928 | "seen_at": 1533670768.73193 929 | }, 930 | { 931 | "nickname": "JWU42", 932 | "hostname": "ELECTRUM.jdubya.info", 933 | "ip_addr": null, 934 | "ports": [ 935 | "t", 936 | "s" 937 | ], 938 | "version": "1.0", 939 | "pruning_limit": 1000, 940 | "seen_at": 1465686119.022112 941 | }, 942 | { 943 | "nickname": null, 944 | "hostname": "electrum.leblancnet.us", 945 | "ip_addr": "69.27.173.238", 946 | "ports": [ 947 | "s50002", 948 | "t50001" 949 | ], 950 | "version": "1.4", 951 | "pruning_limit": 0, 952 | "seen_at": 1533670768.867794 953 | }, 954 | { 955 | "nickname": "RMevaere", 956 | "hostname": "electrum.mevaere.fr", 957 | "ip_addr": null, 958 | "ports": [ 959 | "t0", 960 | "s" 961 | ], 962 | "version": "1.0", 963 | "pruning_limit": 10000, 964 | "seen_at": 1465686119.945171 965 | }, 966 | { 967 | "nickname": "mindspot", 968 | "hostname": "electrum.mindspot.org", 969 | "ip_addr": "172.103.153.90", 970 | "ports": [ 971 | "s50002", 972 | "t50001" 973 | ], 974 | "version": "1.2", 975 | "pruning_limit": 0, 976 | "seen_at": 1533670768.867533 977 | }, 978 | { 979 | "nickname": "neocrypto", 980 | "hostname": "electrum.neocrypto.io", 981 | "ip_addr": null, 982 | "ports": [ 983 | "t", 984 | "s" 985 | ], 986 | "version": "1.0", 987 | "pruning_limit": 10000, 988 | "seen_at": 1465686119.021477 989 | }, 990 | { 991 | "nickname": "netpros", 992 | "hostname": "electrum.netpros.co", 993 | "ip_addr": null, 994 | "ports": [ 995 | "t", 996 | "s" 997 | ], 998 | "version": "1.0", 999 | "pruning_limit": 10000, 1000 | "seen_at": 1465686119.020614 1001 | }, 1002 | { 1003 | "nickname": "NOIP", 1004 | "hostname": "electrum.no-ip.org", 1005 | "ip_addr": null, 1006 | "ports": [ 1007 | "t", 1008 | "s" 1009 | ], 1010 | "version": "1.0", 1011 | "pruning_limit": 10000, 1012 | "seen_at": 1465686119.020677 1013 | }, 1014 | { 1015 | "nickname": null, 1016 | "hostname": "electrum.nute.net", 1017 | "ip_addr": "37.187.141.73", 1018 | "ports": [ 1019 | "s50002" 1020 | ], 1021 | "version": "1.2", 1022 | "pruning_limit": 0, 1023 | "seen_at": 1533670768.86745 1024 | }, 1025 | { 1026 | "nickname": "Online", 1027 | "hostname": "Electrum.Online", 1028 | "ip_addr": null, 1029 | "ports": [ 1030 | "t", 1031 | "s" 1032 | ], 1033 | "version": "1.0", 1034 | "pruning_limit": 10000, 1035 | "seen_at": 1465686119.020231 1036 | }, 1037 | { 1038 | "nickname": null, 1039 | "hostname": "electrum.petrkr.net", 1040 | "ip_addr": "213.168.187.27", 1041 | "ports": [ 1042 | "s50002", 1043 | "t50001" 1044 | ], 1045 | "version": "1.2", 1046 | "pruning_limit": 0, 1047 | "seen_at": 1533670768.8677828 1048 | }, 1049 | { 1050 | "nickname": null, 1051 | "hostname": "electrum.qtornado.com", 1052 | "ip_addr": "88.99.162.199", 1053 | "ports": [ 1054 | "s50002", 1055 | "t50001" 1056 | ], 1057 | "version": "1.4", 1058 | "pruning_limit": 0, 1059 | "seen_at": 1533670768.8676858 1060 | }, 1061 | { 1062 | "nickname": "faro", 1063 | "hostname": "electrum.site2.me", 1064 | "ip_addr": null, 1065 | "ports": [ 1066 | "t40001", 1067 | "s40002" 1068 | ], 1069 | "version": "1.0", 1070 | "pruning_limit": 100, 1071 | "seen_at": 1465686119.020781 1072 | }, 1073 | { 1074 | "nickname": "Snipa", 1075 | "hostname": "electrum.snipanet.com", 1076 | "ip_addr": null, 1077 | "ports": [ 1078 | "t", 1079 | "s" 1080 | ], 1081 | "version": "1.0", 1082 | "pruning_limit": 10000, 1083 | "seen_at": 1465686119.021272 1084 | }, 1085 | { 1086 | "nickname": "stoff-sammlung", 1087 | "hostname": "electrum.stoff-sammlung.de", 1088 | "ip_addr": null, 1089 | "ports": [ 1090 | "t", 1091 | "s" 1092 | ], 1093 | "version": "1.0", 1094 | "pruning_limit": 10000, 1095 | "seen_at": 1465686119.966188 1096 | }, 1097 | { 1098 | "nickname": null, 1099 | "hostname": "electrum.taborsky.cz", 1100 | "ip_addr": "37.205.8.78", 1101 | "ports": [ 1102 | "s50002" 1103 | ], 1104 | "version": "1.4", 1105 | "pruning_limit": 0, 1106 | "seen_at": 1533670768.867468 1107 | }, 1108 | { 1109 | "nickname": "gORlECTRUM", 1110 | "hostname": "ELECTRUM.top-master.com", 1111 | "ip_addr": null, 1112 | "ports": [ 1113 | "t", 1114 | "s" 1115 | ], 1116 | "version": "1.0", 1117 | "pruning_limit": 10000, 1118 | "seen_at": 1465686119.020941 1119 | }, 1120 | { 1121 | "nickname": "trouth", 1122 | "hostname": "electrum.trouth.net", 1123 | "ip_addr": null, 1124 | "ports": [ 1125 | "t", 1126 | "s" 1127 | ], 1128 | "version": "1.0", 1129 | "pruning_limit": 10000, 1130 | "seen_at": 1465686119.02263 1131 | }, 1132 | { 1133 | "nickname": "dogydins", 1134 | "hostname": "electrum.villocq.com", 1135 | "ip_addr": null, 1136 | "ports": [ 1137 | "t", 1138 | "s" 1139 | ], 1140 | "version": "1.0", 1141 | "pruning_limit": 10000, 1142 | "seen_at": 1465686119.040277 1143 | }, 1144 | { 1145 | "nickname": "j_frighten_swo", 1146 | "hostname": "electrum.vom-stausee.de", 1147 | "ip_addr": "37.59.46.112", 1148 | "ports": [ 1149 | "s50002", 1150 | "t50001" 1151 | ], 1152 | "version": "1.2", 1153 | "pruning_limit": 0, 1154 | "seen_at": 1533670768.867701 1155 | }, 1156 | { 1157 | "nickname": "eniac", 1158 | "hostname": "electrum0.snel.it", 1159 | "ip_addr": null, 1160 | "ports": [ 1161 | "t", 1162 | "s" 1163 | ], 1164 | "version": "1.0", 1165 | "pruning_limit": 10000, 1166 | "seen_at": 1465686119.94539 1167 | }, 1168 | { 1169 | "nickname": null, 1170 | "hostname": "electrum2.everynothing.net", 1171 | "ip_addr": "162.212.155.122", 1172 | "ports": [ 1173 | "s50002", 1174 | "t50001" 1175 | ], 1176 | "version": "1.2", 1177 | "pruning_limit": 0, 1178 | "seen_at": 1533670768.867616 1179 | }, 1180 | { 1181 | "nickname": "villocq2", 1182 | "hostname": "electrum2.villocq.com", 1183 | "ip_addr": "91.190.163.21", 1184 | "ports": [ 1185 | "s50002", 1186 | "t50001" 1187 | ], 1188 | "version": "1.2", 1189 | "pruning_limit": 0, 1190 | "seen_at": 1533670768.8675368 1191 | }, 1192 | { 1193 | "nickname": "hachre", 1194 | "hostname": "electrum3.hachre.de", 1195 | "ip_addr": "173.249.52.246", 1196 | "ports": [ 1197 | "s50002", 1198 | "t50001" 1199 | ], 1200 | "version": "1.4", 1201 | "pruning_limit": 0, 1202 | "seen_at": 1533670768.8676941 1203 | }, 1204 | { 1205 | "nickname": null, 1206 | "hostname": "electrumx-bch.cryptonermal.net", 1207 | "ip_addr": "18.233.20.36", 1208 | "ports": [ 1209 | "s50002", 1210 | "t50001" 1211 | ], 1212 | "version": "1.2", 1213 | "pruning_limit": 0, 1214 | "seen_at": 1533670768.5850198 1215 | }, 1216 | { 1217 | "nickname": null, 1218 | "hostname": "electrumx-cash.1209k.com", 1219 | "ip_addr": "2601:602:8802:2091:dcc7:33ff:fe41:6ca6", 1220 | "ports": [ 1221 | "s50002", 1222 | "t50001" 1223 | ], 1224 | "version": "1.4", 1225 | "pruning_limit": 0, 1226 | "seen_at": 1533670768.585003 1227 | }, 1228 | { 1229 | "nickname": null, 1230 | "hostname": "electrumx-core.1209k.com", 1231 | "ip_addr": "2601:602:8802:2091:dcc3:26ff:fe77:bd7f", 1232 | "ports": [ 1233 | "s50002", 1234 | "t50001" 1235 | ], 1236 | "version": "1.4", 1237 | "pruning_limit": 0, 1238 | "seen_at": 1533670768.6282141 1239 | }, 1240 | { 1241 | "nickname": null, 1242 | "hostname": "electrumx.bot.nu", 1243 | "ip_addr": "173.91.90.62", 1244 | "ports": [ 1245 | "s50002", 1246 | "t50001" 1247 | ], 1248 | "version": "1.4", 1249 | "pruning_limit": 0, 1250 | "seen_at": 1533670768.867776 1251 | }, 1252 | { 1253 | "nickname": null, 1254 | "hostname": "electrumx.ddns.net", 1255 | "ip_addr": "169.0.143.207", 1256 | "ports": [ 1257 | "s50002", 1258 | "t50001" 1259 | ], 1260 | "version": "1.2", 1261 | "pruning_limit": 0, 1262 | "seen_at": 1533670768.867609 1263 | }, 1264 | { 1265 | "nickname": null, 1266 | "hostname": "electrumx.hillsideinternet.com", 1267 | "ip_addr": "162.220.47.150", 1268 | "ports": [ 1269 | "s50002", 1270 | "t50001" 1271 | ], 1272 | "version": "1.2", 1273 | "pruning_limit": 0, 1274 | "seen_at": 1533670768.584992 1275 | }, 1276 | { 1277 | "nickname": null, 1278 | "hostname": "electrumx.hopto.org", 1279 | "ip_addr": "89.205.81.5", 1280 | "ports": [ 1281 | "s50002", 1282 | "t50001" 1283 | ], 1284 | "version": "1.2", 1285 | "pruning_limit": 0, 1286 | "seen_at": 1533670768.867797 1287 | }, 1288 | { 1289 | "nickname": null, 1290 | "hostname": "electrumx.nmdps.net", 1291 | "ip_addr": "109.61.102.5", 1292 | "ports": [ 1293 | "s50002", 1294 | "t50001" 1295 | ], 1296 | "version": "1.4", 1297 | "pruning_limit": 0, 1298 | "seen_at": 1533670768.867459 1299 | }, 1300 | { 1301 | "nickname": null, 1302 | "hostname": "electrumx.soon.it", 1303 | "ip_addr": "79.11.31.76", 1304 | "ports": [ 1305 | "s50002", 1306 | "t50001" 1307 | ], 1308 | "version": "1.2", 1309 | "pruning_limit": 0, 1310 | "seen_at": 1533670768.8675761 1311 | }, 1312 | { 1313 | "nickname": null, 1314 | "hostname": "electrumx.westeurope.cloudapp.azure.com", 1315 | "ip_addr": "104.40.216.160", 1316 | "ports": [ 1317 | "s50002", 1318 | "t50001" 1319 | ], 1320 | "version": "1.2", 1321 | "pruning_limit": 0, 1322 | "seen_at": 1533670768.867751 1323 | }, 1324 | { 1325 | "nickname": "Pielectrum_TOR", 1326 | "hostname": "electrumx67xeros.onion", 1327 | "ip_addr": null, 1328 | "ports": [ 1329 | "t", 1330 | "s" 1331 | ], 1332 | "version": "1.0", 1333 | "pruning_limit": 10000, 1334 | "seen_at": 1465686119.944136 1335 | }, 1336 | { 1337 | "nickname": null, 1338 | "hostname": "electrumxhqdsmlu.onion", 1339 | "ip_addr": null, 1340 | "ports": [ 1341 | "t50001" 1342 | ], 1343 | "version": "1.2", 1344 | "pruning_limit": 0, 1345 | "seen_at": 1533670768.628143 1346 | }, 1347 | { 1348 | "nickname": null, 1349 | "hostname": "enode.duckdns.org", 1350 | "ip_addr": "75.159.6.167", 1351 | "ports": [ 1352 | "s50002", 1353 | "t50001" 1354 | ], 1355 | "version": "1.2", 1356 | "pruning_limit": 0, 1357 | "seen_at": 1533670768.8676028 1358 | }, 1359 | { 1360 | "nickname": "dunp", 1361 | "hostname": "erbium1.sytes.net", 1362 | "ip_addr": "46.246.40.134", 1363 | "ports": [ 1364 | "s50002", 1365 | "t50001" 1366 | ], 1367 | "version": "1.4", 1368 | "pruning_limit": 0, 1369 | "seen_at": 1533670768.867627 1370 | }, 1371 | { 1372 | "nickname": "j_fdk_b_tor", 1373 | "hostname": "fdkbwjykvl2f3hup.onion", 1374 | "ip_addr": null, 1375 | "ports": [ 1376 | "t", 1377 | "s" 1378 | ], 1379 | "version": "1.0", 1380 | "pruning_limit": 10000, 1381 | "seen_at": 1465686119.020525 1382 | }, 1383 | { 1384 | "nickname": "j_fdk_h_tor", 1385 | "hostname": "fdkhv2bb7hqel2e7.onion", 1386 | "ip_addr": null, 1387 | "ports": [ 1388 | "t", 1389 | "s" 1390 | ], 1391 | "version": "1.0", 1392 | "pruning_limit": 10000, 1393 | "seen_at": 1465686119.021149 1394 | }, 1395 | { 1396 | "nickname": "electron", 1397 | "hostname": "gh05.geekhosters.com", 1398 | "ip_addr": null, 1399 | "ports": [ 1400 | "t", 1401 | "s" 1402 | ], 1403 | "version": "1.0", 1404 | "pruning_limit": 10000, 1405 | "seen_at": 1465686119.945288 1406 | }, 1407 | { 1408 | "nickname": "j_fdk_h", 1409 | "hostname": "h.1209k.com", 1410 | "ip_addr": null, 1411 | "ports": [ 1412 | "t", 1413 | "s" 1414 | ], 1415 | "version": "1.0", 1416 | "pruning_limit": 10000, 1417 | "seen_at": 1465686119.020997 1418 | }, 1419 | { 1420 | "nickname": null, 1421 | "hostname": "helicarrier.bauerj.eu", 1422 | "ip_addr": "178.32.88.133", 1423 | "ports": [ 1424 | "s50002", 1425 | "t50001" 1426 | ], 1427 | "version": "1.4", 1428 | "pruning_limit": 0, 1429 | "seen_at": 1533670768.867583 1430 | }, 1431 | { 1432 | "nickname": null, 1433 | "hostname": "hetzner01.fischl-online.de", 1434 | "ip_addr": "5.9.124.124", 1435 | "ports": [ 1436 | "s50002" 1437 | ], 1438 | "version": "1.2", 1439 | "pruning_limit": 0, 1440 | "seen_at": 1533670768.867667 1441 | }, 1442 | { 1443 | "nickname": null, 1444 | "hostname": "hsmiths4fyqlw5xw.onion", 1445 | "ip_addr": null, 1446 | "ports": [ 1447 | "s50002", 1448 | "t50001" 1449 | ], 1450 | "version": "1.4", 1451 | "pruning_limit": 0, 1452 | "seen_at": 1533670768.867569 1453 | }, 1454 | { 1455 | "nickname": null, 1456 | "hostname": "hsmiths5mjk6uijs.onion", 1457 | "ip_addr": null, 1458 | "ports": [ 1459 | "s50002", 1460 | "t50001" 1461 | ], 1462 | "version": "1.4", 1463 | "pruning_limit": 0, 1464 | "seen_at": 1533670768.86774 1465 | }, 1466 | { 1467 | "nickname": "DEVV", 1468 | "hostname": "ilikehuskies.no-ip.org", 1469 | "ip_addr": null, 1470 | "ports": [ 1471 | "t", 1472 | "s" 1473 | ], 1474 | "version": "1.0", 1475 | "pruning_limit": 10000, 1476 | "seen_at": 1465686119.022576 1477 | }, 1478 | { 1479 | "nickname": null, 1480 | "hostname": "ip101.ip-54-37-91.eu", 1481 | "ip_addr": "54.37.91.101", 1482 | "ports": [ 1483 | "s50002", 1484 | "t50001" 1485 | ], 1486 | "version": "1.1", 1487 | "pruning_limit": 0, 1488 | "seen_at": 1533670768.696357 1489 | }, 1490 | { 1491 | "nickname": null, 1492 | "hostname": "ip119.ip-54-37-91.eu", 1493 | "ip_addr": "54.37.91.119", 1494 | "ports": [ 1495 | "s50002", 1496 | "t50001" 1497 | ], 1498 | "version": "1.1", 1499 | "pruning_limit": 0, 1500 | "seen_at": 1533670768.867723 1501 | }, 1502 | { 1503 | "nickname": null, 1504 | "hostname": "ip120.ip-54-37-91.eu", 1505 | "ip_addr": "54.37.91.120", 1506 | "ports": [ 1507 | "s50002", 1508 | "t50001" 1509 | ], 1510 | "version": "1.1", 1511 | "pruning_limit": 0, 1512 | "seen_at": 1533670768.867553 1513 | }, 1514 | { 1515 | "nickname": null, 1516 | "hostname": "ip239.ip-54-36-234.eu", 1517 | "ip_addr": "54.36.234.239", 1518 | "ports": [ 1519 | "s50002", 1520 | "t50001" 1521 | ], 1522 | "version": "1.1", 1523 | "pruning_limit": 0, 1524 | "seen_at": 1533670768.867719 1525 | }, 1526 | { 1527 | "nickname": "fydel_tor", 1528 | "hostname": "ixxdq23ewy77sau6.onion", 1529 | "ip_addr": null, 1530 | "ports": [ 1531 | "t", 1532 | "s" 1533 | ], 1534 | "version": "1.0", 1535 | "pruning_limit": 10000, 1536 | "seen_at": 1465686119.02234 1537 | }, 1538 | { 1539 | "nickname": null, 1540 | "hostname": "iy5jbpzok4spzetr.onion", 1541 | "ip_addr": null, 1542 | "ports": [ 1543 | "s50002", 1544 | "t50001" 1545 | ], 1546 | "version": "1.4", 1547 | "pruning_limit": 0, 1548 | "seen_at": 1533670768.867765 1549 | }, 1550 | { 1551 | "nickname": "JWU42[b]", 1552 | "hostname": "jwu42.hopto.org", 1553 | "ip_addr": null, 1554 | "ports": [ 1555 | "t50003", 1556 | "s50004" 1557 | ], 1558 | "version": "1.0", 1559 | "pruning_limit": 1000, 1560 | "seen_at": 1465686119.022186 1561 | }, 1562 | { 1563 | "nickname": null, 1564 | "hostname": "kirsche.emzy.de", 1565 | "ip_addr": "78.47.61.83", 1566 | "ports": [ 1567 | "s50002", 1568 | "t50001" 1569 | ], 1570 | "version": "1.4", 1571 | "pruning_limit": 0, 1572 | "seen_at": 1533670768.867726 1573 | }, 1574 | { 1575 | "nickname": null, 1576 | "hostname": "liyqfqfsiewcsumb.onion", 1577 | "ip_addr": null, 1578 | "ports": [ 1579 | "s50003", 1580 | "t50001" 1581 | ], 1582 | "version": "1.2", 1583 | "pruning_limit": 0, 1584 | "seen_at": 1533670768.867557 1585 | }, 1586 | { 1587 | "nickname": null, 1588 | "hostname": "luggscoqbymhvnkp.onion", 1589 | "ip_addr": null, 1590 | "ports": [ 1591 | "t80" 1592 | ], 1593 | "version": "1.4", 1594 | "pruning_limit": 0, 1595 | "seen_at": 1533670768.8674839 1596 | }, 1597 | { 1598 | "nickname": "j_fdk_mash_tor", 1599 | "hostname": "mashtk6hmnysevfj.onion", 1600 | "ip_addr": null, 1601 | "ports": [ 1602 | "t", 1603 | "s" 1604 | ], 1605 | "version": "1.0", 1606 | "pruning_limit": 10000, 1607 | "seen_at": 1465686119.021092 1608 | }, 1609 | { 1610 | "nickname": null, 1611 | "hostname": "mooo.not.fyi", 1612 | "ip_addr": "71.239.122.162", 1613 | "ports": [ 1614 | "s50012", 1615 | "t50011" 1616 | ], 1617 | "version": "1.1", 1618 | "pruning_limit": 0, 1619 | "seen_at": 1533670768.867733 1620 | }, 1621 | { 1622 | "nickname": null, 1623 | "hostname": "ndnd.selfhost.eu", 1624 | "ip_addr": "217.233.81.39", 1625 | "ports": [ 1626 | "s50002", 1627 | "t50001" 1628 | ], 1629 | "version": "1.4", 1630 | "pruning_limit": 0, 1631 | "seen_at": 1533670768.86773 1632 | }, 1633 | { 1634 | "nickname": null, 1635 | "hostname": "ndndword5lpb7eex.onion", 1636 | "ip_addr": null, 1637 | "ports": [ 1638 | "t50001" 1639 | ], 1640 | "version": "1.4", 1641 | "pruning_limit": 0, 1642 | "seen_at": 1533670768.867682 1643 | }, 1644 | { 1645 | "nickname": null, 1646 | "hostname": "node.erratic.space", 1647 | "ip_addr": "69.10.143.103", 1648 | "ports": [ 1649 | "s50002", 1650 | "t50001" 1651 | ], 1652 | "version": "1.2", 1653 | "pruning_limit": 0, 1654 | "seen_at": 1533670768.8675609 1655 | }, 1656 | { 1657 | "nickname": null, 1658 | "hostname": "node.ispol.sk", 1659 | "ip_addr": "193.58.196.212", 1660 | "ports": [ 1661 | "s50002", 1662 | "t50001" 1663 | ], 1664 | "version": "1.2", 1665 | "pruning_limit": 0, 1666 | "seen_at": 1533670768.8677151 1667 | }, 1668 | { 1669 | "nickname": null, 1670 | "hostname": "orannis.com", 1671 | "ip_addr": "50.35.67.146", 1672 | "ports": [ 1673 | "s50002", 1674 | "t50001" 1675 | ], 1676 | "version": "1.4", 1677 | "pruning_limit": 0, 1678 | "seen_at": 1533670768.867517 1679 | }, 1680 | { 1681 | "nickname": "selavi_tor", 1682 | "hostname": "ozahtqwp25chjdjd.onion", 1683 | "ip_addr": null, 1684 | "ports": [ 1685 | "s50002", 1686 | "t50001" 1687 | ], 1688 | "version": "1.4", 1689 | "pruning_limit": 0, 1690 | "seen_at": 1533670768.628438 1691 | }, 1692 | { 1693 | "nickname": null, 1694 | "hostname": "qmebr.spdns.org", 1695 | "ip_addr": "87.122.92.144", 1696 | "ports": [ 1697 | "s50002", 1698 | "t50001" 1699 | ], 1700 | "version": "1.2", 1701 | "pruning_limit": 0, 1702 | "seen_at": 1533670768.867509 1703 | }, 1704 | { 1705 | "nickname": null, 1706 | "hostname": "qtornadoklbgdyww.onion", 1707 | "ip_addr": null, 1708 | "ports": [ 1709 | "s50002", 1710 | "t50001" 1711 | ], 1712 | "version": "1.4", 1713 | "pruning_limit": 0, 1714 | "seen_at": 1533670768.628462 1715 | }, 1716 | { 1717 | "nickname": null, 1718 | "hostname": "rbx.curalle.ovh", 1719 | "ip_addr": "176.31.252.219", 1720 | "ports": [ 1721 | "s50002" 1722 | ], 1723 | "version": "1.4", 1724 | "pruning_limit": 0, 1725 | "seen_at": 1533670768.867744 1726 | }, 1727 | { 1728 | "nickname": "cplus_tor", 1729 | "hostname": "rvm6c7kj63mtztgn.onion", 1730 | "ip_addr": null, 1731 | "ports": [ 1732 | "t", 1733 | "s" 1734 | ], 1735 | "version": "1.0", 1736 | "pruning_limit": 10000, 1737 | "seen_at": 1465686119.965912 1738 | }, 1739 | { 1740 | "nickname": null, 1741 | "hostname": "ryba-btc.noip.pl", 1742 | "ip_addr": "109.199.70.182", 1743 | "ports": [ 1744 | "s50002", 1745 | "t50001" 1746 | ], 1747 | "version": "1.4", 1748 | "pruning_limit": 0, 1749 | "seen_at": 1533670768.86748 1750 | }, 1751 | { 1752 | "nickname": null, 1753 | "hostname": "rybabtcmltnlykbd.onion", 1754 | "ip_addr": null, 1755 | "ports": [ 1756 | "s50002", 1757 | "t50001" 1758 | ], 1759 | "version": "1.4", 1760 | "pruning_limit": 0, 1761 | "seen_at": 1533670768.867521 1762 | }, 1763 | { 1764 | "nickname": null, 1765 | "hostname": "s7clinmo4cazmhul.onion", 1766 | "ip_addr": null, 1767 | "ports": [ 1768 | "t50001" 1769 | ], 1770 | "version": "1.4", 1771 | "pruning_limit": 0, 1772 | "seen_at": 1533670768.867476 1773 | }, 1774 | { 1775 | "nickname": null, 1776 | "hostname": "shogoth.no-ip.info", 1777 | "ip_addr": "155.4.117.22", 1778 | "ports": [ 1779 | "s50002", 1780 | "t50001" 1781 | ], 1782 | "version": "1.2", 1783 | "pruning_limit": 0, 1784 | "seen_at": 1533670768.8675132 1785 | }, 1786 | { 1787 | "nickname": null, 1788 | "hostname": "spv.48.org", 1789 | "ip_addr": "81.171.27.138", 1790 | "ports": [ 1791 | "s50002", 1792 | "t50003" 1793 | ], 1794 | "version": "1.2", 1795 | "pruning_limit": 0, 1796 | "seen_at": 1533670768.8674932 1797 | }, 1798 | { 1799 | "nickname": null, 1800 | "hostname": "such.ninja", 1801 | "ip_addr": "163.172.61.154", 1802 | "ports": [ 1803 | "s50002", 1804 | "t50001" 1805 | ], 1806 | "version": "1.1", 1807 | "pruning_limit": 0, 1808 | "seen_at": 1533670768.867708 1809 | }, 1810 | { 1811 | "nickname": null, 1812 | "hostname": "tardis.bauerj.eu", 1813 | "ip_addr": "51.15.138.64", 1814 | "ports": [ 1815 | "s50002", 1816 | "t50001" 1817 | ], 1818 | "version": "1.4", 1819 | "pruning_limit": 0, 1820 | "seen_at": 1533670768.867758 1821 | }, 1822 | { 1823 | "nickname": null, 1824 | "hostname": "technetium.network", 1825 | "ip_addr": "96.27.8.242", 1826 | "ports": [ 1827 | "s50002" 1828 | ], 1829 | "version": "1.2", 1830 | "pruning_limit": 0, 1831 | "seen_at": 1533670768.867541 1832 | }, 1833 | { 1834 | "nickname": "ulrichard", 1835 | "hostname": "ulrichard.ch", 1836 | "ip_addr": null, 1837 | "ports": [ 1838 | "t", 1839 | "s" 1840 | ], 1841 | "version": "1.0", 1842 | "pruning_limit": 10000, 1843 | "seen_at": 1465686119.020178 1844 | }, 1845 | { 1846 | "nickname": "ECO", 1847 | "hostname": "ultra-ecoelectrum.my-gateway.de", 1848 | "ip_addr": null, 1849 | "ports": [ 1850 | "t", 1851 | "s" 1852 | ], 1853 | "version": "1.0", 1854 | "pruning_limit": 100, 1855 | "seen_at": 1465686119.020727 1856 | }, 1857 | { 1858 | "nickname": "US", 1859 | "hostname": "us.electrum.be", 1860 | "ip_addr": "208.110.73.107", 1861 | "ports": [ 1862 | "s50002", 1863 | "t50001" 1864 | ], 1865 | "version": "1.1", 1866 | "pruning_limit": 0, 1867 | "seen_at": 1533670768.494337 1868 | }, 1869 | { 1870 | "nickname": "hsmiths2", 1871 | "hostname": "VPS.hsmiths.com", 1872 | "ip_addr": "51.15.77.78", 1873 | "ports": [ 1874 | "s50002", 1875 | "t50001" 1876 | ], 1877 | "version": "1.4", 1878 | "pruning_limit": 0, 1879 | "seen_at": 1533670768.8675961 1880 | }, 1881 | { 1882 | "nickname": null, 1883 | "hostname": "wsw6tua3xl24gsmi264zaep6seppjyrkyucpsmuxnjzyt3f3j6swshad.onion", 1884 | "ip_addr": null, 1885 | "ports": [ 1886 | "s50002", 1887 | "t50001" 1888 | ], 1889 | "version": "1.4", 1890 | "pruning_limit": 0, 1891 | "seen_at": 1533670768.86779 1892 | }, 1893 | { 1894 | "nickname": null, 1895 | "hostname": "y4td57fxytoo5ki7.onion", 1896 | "ip_addr": null, 1897 | "ports": [ 1898 | "s50002", 1899 | "t50001" 1900 | ], 1901 | "version": "1.1", 1902 | "pruning_limit": 0, 1903 | "seen_at": 1533670768.867754 1904 | }, 1905 | { 1906 | "nickname": null, 1907 | "hostname": "yuio.top", 1908 | "ip_addr": "118.86.185.36", 1909 | "ports": [ 1910 | "s50003", 1911 | "t50001" 1912 | ], 1913 | "version": "1.2", 1914 | "pruning_limit": 0, 1915 | "seen_at": 1533670768.8676789 1916 | } 1917 | ] --------------------------------------------------------------------------------