├── 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 |%s' % (t, n, label or n)
40 |
41 |
42 | async def homepage(request):
43 | conn = request.app['conn']
44 | t = HTML_HDR
45 | t += "\n%s\n
Donations: %s
' % linkage(donate) 56 | t += 'Top block: %s
' % linkage(top_blk['block_height']) 57 | 58 | 59 | t += ''' 60 |