Browse Source

Begin work on proper server

master
Neil Booth 8 years ago
parent
commit
334ffdaa4f
  1. 35
      electrumx_rpc.py
  2. 7
      lib/hash.py
  3. 8
      lib/util.py
  4. 5
      query.py
  5. 0
      samples/scripts/env/DAEMON_HOST
  6. 0
      samples/scripts/env/DAEMON_PASSWORD
  7. 1
      samples/scripts/env/DAEMON_PORT
  8. 0
      samples/scripts/env/DAEMON_USERNAME
  9. 2
      samples/scripts/env/RPC_PORT
  10. 1
      samples/scripts/env/SSL_PORT
  11. 0
      samples/scripts/env/TCP_PORT
  12. 128
      server/controller.py
  13. 52
      server/db.py
  14. 38
      server/env.py
  15. 206
      server/protocol.py
  16. 60
      server/rpc.py
  17. 1
      server/version.py
  18. 10
      server_main.py

35
electrumx_rpc.py

@ -6,16 +6,29 @@
import argparse import argparse
import asyncio import asyncio
import json import json
from functools import partial
from os import environ from os import environ
import aiohttp
class RPCClient(asyncio.Protocol):
async def send(url, payload): def __init__(self, loop):
data = json.dumps(payload) self.loop = loop
async with aiohttp.post(url, data = data) as resp: def connection_made(self, transport):
return await resp.json() self.transport = transport
def connection_lost(self, exc):
self.loop.stop()
def send(self, payload):
data = json.dumps(payload) + '\n'
self.transport.write(data.encode())
def data_received(self, data):
payload = json.loads(data.decode())
self.transport.close()
print(json.dumps(payload, indent=4, sort_keys=True))
def main(): def main():
@ -30,18 +43,20 @@ def main():
if args.port is None: if args.port is None:
args.port = int(environ.get('ELECTRUMX_RPC_PORT', 8000)) args.port = int(environ.get('ELECTRUMX_RPC_PORT', 8000))
url = 'http://127.0.0.1:{:d}/'.format(args.port)
payload = {'method': args.command[0], 'params': args.command[1:]} payload = {'method': args.command[0], 'params': args.command[1:]}
task = send(url, payload)
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
proto_factory = partial(RPCClient, loop)
coro = loop.create_connection(proto_factory, 'localhost', args.port)
try: try:
result = loop.run_until_complete(task) transport, protocol = loop.run_until_complete(coro)
protocol.send(payload)
loop.run_forever()
except OSError:
print('error connecting - is ElectrumX running?')
finally: finally:
loop.close() loop.close()
print(result)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

7
lib/hash.py

@ -31,6 +31,13 @@ def hash160(x):
return ripemd160(sha256(x)) return ripemd160(sha256(x))
def hash_to_str(x):
'''Converts a big-endian binary hash to a little-endian hex string, as
shown in block explorers, etc.
'''
return bytes(reversed(x)).hex()
class InvalidBase58String(Exception): class InvalidBase58String(Exception):
pass pass

8
lib/util.py

@ -2,10 +2,18 @@
# and warranty status of this software. # and warranty status of this software.
import array import array
import logging
import sys import sys
from collections import Container, Mapping from collections import Container, Mapping
class LoggedClass(object):
def __init__(self):
self.logger = logging.getLogger(self.__class__.__name__)
self.logger.setLevel(logging.INFO)
# Method decorator. To be used for calculations that will always # Method decorator. To be used for calculations that will always
# deliver the same result. The method cannot take any arguments # deliver the same result. The method cannot take any arguments
# and should be accessed as an attribute. # and should be accessed as an attribute.

5
query.py

@ -8,6 +8,7 @@ import sys
from server.env import Env from server.env import Env
from server.db import DB from server.db import DB
from lib.hash import hash_to_str
def main(): def main():
@ -27,13 +28,13 @@ def main():
n = None n = None
for n, (tx_hash, height) in enumerate(db.get_history(hash168, limit)): for n, (tx_hash, height) in enumerate(db.get_history(hash168, limit)):
print('History #{:d}: hash: {} height: {:d}' print('History #{:d}: hash: {} height: {:d}'
.format(n + 1, bytes(reversed(tx_hash)).hex(), height)) .format(n + 1, hash_to_str(tx_hash), height))
if n is None: if n is None:
print('No history') print('No history')
n = None n = None
for n, utxo in enumerate(db.get_utxos(hash168, limit)): for n, utxo in enumerate(db.get_utxos(hash168, limit)):
print('UTXOs #{:d}: hash: {} pos: {:d} height: {:d} value: {:d}' print('UTXOs #{:d}: hash: {} pos: {:d} height: {:d} value: {:d}'
.format(n + 1, bytes(reversed(utxo.tx_hash)).hex(), .format(n + 1, hash_to_str(utxo.tx_hash),
utxo.tx_pos, utxo.height, utxo.value)) utxo.tx_pos, utxo.height, utxo.value))
if n is None: if n is None:
print('No UTXOs') print('No UTXOs')

0
samples/scripts/env/RPC_HOST → samples/scripts/env/DAEMON_HOST

0
samples/scripts/env/RPC_PASSWORD → samples/scripts/env/DAEMON_PASSWORD

1
samples/scripts/env/DAEMON_PORT

@ -0,0 +1 @@
8332

0
samples/scripts/env/RPC_USERNAME → samples/scripts/env/DAEMON_USERNAME

2
samples/scripts/env/RPC_PORT

@ -1 +1 @@
8332 8000

1
samples/scripts/env/SSL_PORT

@ -0,0 +1 @@
50002

0
samples/scripts/env/TCP_PORT

128
server/server.py → server/controller.py

@ -3,76 +3,134 @@
import asyncio import asyncio
import json import json
import logging
import signal import signal
import traceback
from functools import partial from functools import partial
import aiohttp import aiohttp
from server.db import DB from server.db import DB
from server.rpc import ElectrumRPCServer from server.protocol import ElectrumX, LocalRPC
from lib.hash import sha256, hash_to_str, Base58
from lib.util import LoggedClass
class Server(object): class Controller(LoggedClass):
def __init__(self, env): def __init__(self, env):
super().__init__()
self.env = env self.env = env
self.db = DB(env) self.db = DB(env)
self.block_cache = BlockCache(env, self.db) self.block_cache = BlockCache(env, self.db)
self.rpc_server = ElectrumRPCServer(self) self.servers = []
self.sessions = set()
self.addresses = {}
self.jobs = set()
self.peers = {}
def start(self, loop):
env = self.env
protocol = partial(LocalRPC, self)
if env.rpc_port is not None:
host = 'localhost'
rpc_server = loop.create_server(protocol, host, env.rpc_port)
self.servers.append(loop.run_until_complete(rpc_server))
self.logger.info('RPC server listening on {}:{:d}'
.format(host, env.rpc_port))
protocol = partial(ElectrumX, self, env)
if env.tcp_port is not None:
tcp_server = loop.create_server(protocol, env.host, env.tcp_port)
self.servers.append(loop.run_until_complete(tcp_server))
self.logger.info('TCP server listening on {}:{:d}'
.format(env.host, env.tcp_port))
if env.ssl_port is not None:
ssl_server = loop.create_server(protocol, env.host, env.ssl_port)
self.servers.append(loop.run_until_complete(ssl_server))
self.logger.info('SSL server listening on {}:{:d}'
.format(env.host, env.ssl_port))
coros = [
self.reap_jobs(),
self.block_cache.catch_up(),
self.block_cache.process_cache()
]
self.tasks = [asyncio.ensure_future(coro) for coro in coros]
# Signal handlers # Signal handlers
loop = asyncio.get_event_loop()
for signame in ('SIGINT', 'SIGTERM'): for signame in ('SIGINT', 'SIGTERM'):
loop.add_signal_handler(getattr(signal, signame), loop.add_signal_handler(getattr(signal, signame),
partial(self.on_signal, signame)) partial(self.on_signal, signame))
coros = self.rpc_server.tasks(env.electrumx_rpc_port) return self.tasks
coros += [self.block_cache.catch_up(),
self.block_cache.process_cache()]
self.tasks = [asyncio.ensure_future(coro) for coro in coros]
async def handle_rpc_getinfo(self, params): def stop(self):
return None, { for server in self.servers:
'blocks': self.db.height, server.close()
'peers': 0,
'sessions': 0,
'watched': 0,
'cached': 0,
}
async def handle_rpc_sessions(self, params): def add_session(self, session):
return None, [] self.sessions.add(session)
async def handle_rpc_numsessions(self, params): def remove_session(self, session):
return None, 0 self.sessions.remove(session)
async def handle_rpc_peers(self, params): def add_job(self, coro):
return None, [] '''Queue a job for asynchronous processing.'''
self.jobs.add(asyncio.ensure_future(coro))
async def handle_rpc_banner_update(self, params): async def reap_jobs(self):
return None, 'FIXME' while True:
jobs = set()
for job in self.jobs:
if job.done():
try:
job.result()
except Exception as e:
traceback.print_exc()
else:
jobs.add(job)
self.logger.info('reaped {:d} jobs, {:d} jobs pending'
.format(len(self.jobs) - len(jobs), len(jobs)))
self.jobs = jobs
await asyncio.sleep(5)
def on_signal(self, signame): def on_signal(self, signame):
logging.warning('received {} signal, preparing to shut down' self.logger.warning('received {} signal, preparing to shut down'
.format(signame)) .format(signame))
for task in self.tasks: for task in self.tasks:
task.cancel() task.cancel()
def async_tasks(self): def address_status(self, hash168):
return self.tasks '''Returns status as 32 bytes.'''
status = self.addresses.get(hash168)
if status is None:
status = ''.join(
'{}:{:d}:'.format(hash_to_str(tx_hash), height)
for tx_hash, height in self.db.get_history(hash168)
)
if status:
status = sha256(status.encode())
self.addresses[hash168] = status
return status
class BlockCache(object): def get_peers(self):
'''Returns a dictionary of IRC nick to (ip, host, ports) tuples, one
per peer.'''
return self.peers
class BlockCache(LoggedClass):
'''Requests blocks ahead of time from the daemon. Serves them '''Requests blocks ahead of time from the daemon. Serves them
to the blockchain processor.''' to the blockchain processor.'''
def __init__(self, env, db): def __init__(self, env, db):
self.logger = logging.getLogger('BlockCache') super().__init__()
self.logger.setLevel(logging.INFO)
self.db = db self.db = db
self.rpc_url = env.rpc_url self.daemon_url = env.daemon_url
# Cache target size is in MB. Has little effect on sync time. # Cache target size is in MB. Has little effect on sync time.
self.cache_limit = 10 self.cache_limit = 10
self.daemon_height = 0 self.daemon_height = 0
@ -82,7 +140,7 @@ class BlockCache(object):
self.recent_sizes = [] self.recent_sizes = []
self.ave_size = 0 self.ave_size = 0
self.logger.info('using RPC URL {}'.format(self.rpc_url)) self.logger.info('using daemon URL {}'.format(self.daemon_url))
async def process_cache(self): async def process_cache(self):
while True: while True:
@ -173,7 +231,7 @@ class BlockCache(object):
data = json.dumps(payload) data = json.dumps(payload)
while True: while True:
try: try:
async with aiohttp.post(self.rpc_url, data = data) as resp: async with aiohttp.post(self.daemon_url, data = data) as resp:
result = await resp.json() result = await resp.json()
except asyncio.CancelledError: except asyncio.CancelledError:
raise raise

52
server/db.py

@ -7,16 +7,16 @@ import itertools
import os import os
import struct import struct
import time import time
from binascii import hexlify, unhexlify
from bisect import bisect_right from bisect import bisect_right
from collections import defaultdict, namedtuple from collections import defaultdict, namedtuple
from functools import partial from functools import partial
import logging
import plyvel import plyvel
from lib.coins import Bitcoin
from lib.script import ScriptPubKey from lib.script import ScriptPubKey
from lib.util import LoggedClass
from lib.hash import hash_to_str
# History can hold approx. 65536 * HIST_ENTRIES_PER_KEY entries # History can hold approx. 65536 * HIST_ENTRIES_PER_KEY entries
HIST_ENTRIES_PER_KEY = 1024 HIST_ENTRIES_PER_KEY = 1024
@ -25,14 +25,13 @@ ADDR_TX_HASH_LEN = 4
UTXO_TX_HASH_LEN = 4 UTXO_TX_HASH_LEN = 4
UTXO = namedtuple("UTXO", "tx_num tx_pos tx_hash height value") UTXO = namedtuple("UTXO", "tx_num tx_pos tx_hash height value")
def formatted_time(t): def formatted_time(t):
t = int(t) t = int(t)
return '{:d}d {:02d}h {:02d}m {:02d}s'.format( return '{:d}d {:02d}h {:02d}m {:02d}s'.format(
t // 86400, (t % 86400) // 3600, (t % 3600) // 60, t % 60) t // 86400, (t % 86400) // 3600, (t % 3600) // 60, t % 60)
class UTXOCache(object): class UTXOCache(LoggedClass):
'''An in-memory UTXO cache, representing all changes to UTXO state '''An in-memory UTXO cache, representing all changes to UTXO state
since the last DB flush. since the last DB flush.
@ -85,8 +84,7 @@ class UTXOCache(object):
''' '''
def __init__(self, parent, db, coin): def __init__(self, parent, db, coin):
self.logger = logging.getLogger('UTXO') super().__init__()
self.logger.setLevel(logging.INFO)
self.parent = parent self.parent = parent
self.coin = coin self.coin = coin
self.cache = {} self.cache = {}
@ -126,7 +124,7 @@ class UTXOCache(object):
# d5d27987d2a3dfc724e359870c6644b40e497bdc0589a033220fe15429d88599 # d5d27987d2a3dfc724e359870c6644b40e497bdc0589a033220fe15429d88599
#if key in self.cache: #if key in self.cache:
# self.logger.info('duplicate tx hash {}' # self.logger.info('duplicate tx hash {}'
# .format(bytes(reversed(tx_hash)).hex())) # .format(hash_to_str(tx_hash)))
self.cache[key] = hash168 + tx_numb + pack('<Q', txout.value) self.cache[key] = hash168 + tx_numb + pack('<Q', txout.value)
@ -160,7 +158,7 @@ class UTXOCache(object):
if data is None: if data is None:
# Uh-oh, this should not happen... # Uh-oh, this should not happen...
self.logger.error('found no UTXO for {} / {:d} key {}' self.logger.error('found no UTXO for {} / {:d} key {}'
.format(bytes(reversed(prevout.hash)).hex(), .format(hash_to_str(prevout.hash),
prevout.n, bytes(key).hex())) prevout.n, bytes(key).hex()))
return hash168 return hash168
@ -194,7 +192,7 @@ class UTXOCache(object):
# Assuming the DB is not corrupt, this indicates a # Assuming the DB is not corrupt, this indicates a
# successful spend of a non-standard script # successful spend of a non-standard script
# self.logger.info('ignoring spend of non-standard UTXO {} / {:d}' # self.logger.info('ignoring spend of non-standard UTXO {} / {:d}'
# .format(bytes(reversed(tx_hash)).hex(), idx))) # .format(hash_to_str(tx_hash), idx)))
return None return None
if len(data) == 25: if len(data) == 25:
@ -277,14 +275,13 @@ class UTXOCache(object):
self.adds = self.cache_hits = self.db_deletes = 0 self.adds = self.cache_hits = self.db_deletes = 0
class DB(object): class DB(LoggedClass):
class Error(Exception): class Error(Exception):
pass pass
def __init__(self, env): def __init__(self, env):
self.logger = logging.getLogger('DB') super().__init__()
self.logger.setLevel(logging.INFO)
# Meta # Meta
self.tx_hash_file_size = 16 * 1024 * 1024 self.tx_hash_file_size = 16 * 1024 * 1024
@ -293,6 +290,7 @@ class DB(object):
self.next_cache_check = 0 self.next_cache_check = 0
self.last_flush = time.time() self.last_flush = time.time()
self.coin = env.coin self.coin = env.coin
self.current_header = None
# Chain state (initialize to genesis in case of new DB) # Chain state (initialize to genesis in case of new DB)
self.db_height = -1 self.db_height = -1
@ -358,7 +356,7 @@ class DB(object):
def read_state(self, db): def read_state(self, db):
state = db.get(b'state') state = db.get(b'state')
state = ast.literal_eval(state.decode('ascii')) state = ast.literal_eval(state.decode())
if state['genesis'] != self.coin.GENESIS_HASH: if state['genesis'] != self.coin.GENESIS_HASH:
raise self.Error('DB genesis hash {} does not match coin {}' raise self.Error('DB genesis hash {} does not match coin {}'
.format(state['genesis_hash'], .format(state['genesis_hash'],
@ -448,7 +446,7 @@ class DB(object):
'utxo_flush_count': self.utxo_flush_count, 'utxo_flush_count': self.utxo_flush_count,
'wall_time': self.wall_time, 'wall_time': self.wall_time,
} }
batch.put(b'state', repr(state).encode('ascii')) batch.put(b'state', repr(state).encode())
def flush_utxos(self, batch): def flush_utxos(self, batch):
self.logger.info('flushing UTXOs: {:,d} txs and {:,d} blocks' self.logger.info('flushing UTXOs: {:,d} txs and {:,d} blocks'
@ -675,3 +673,27 @@ class DB(object):
'''Returns all the UTXOs for an address sorted by height and '''Returns all the UTXOs for an address sorted by height and
position in the block.''' position in the block.'''
return sorted(self.get_utxos(hash168, limit=None)) return sorted(self.get_utxos(hash168, limit=None))
def encode_header(self):
if self.height == -1:
return None
header = self.read_headers(self.height, 1)
unpack = struct.unpack
version, = unpack('<I', header[:4])
timestamp, bits, nonce = unpack('<III', header[68:80])
return {
'block_height': self.height,
'version': version,
'prev_block_hash': hash_to_str(header[4:36]),
'merkle_root': hash_to_str(header[36:68]),
'timestamp': timestamp,
'bits': bits,
'nonce': nonce,
}
def get_current_header(self):
# FIXME: clear current_header on new block
if self.current_header is None:
self.current_header = self.encode_header()
return self.current_header

38
server/env.py

@ -1,29 +1,35 @@
# See the file "LICENSE" for information about the copyright # See the file "LICENSE" for information about the copyright
# and warranty status of this software. # and warranty status of this software.
import logging
from os import environ from os import environ
from lib.coins import Coin from lib.coins import Coin
from lib.util import LoggedClass
class Env(object): class Env(LoggedClass):
'''Wraps environment configuration.''' '''Wraps environment configuration.'''
class Error(Exception): class Error(Exception):
pass pass
def __init__(self): def __init__(self):
self.logger = logging.getLogger('Env') super().__init__()
self.logger.setLevel(logging.INFO)
coin_name = self.default('COIN', 'Bitcoin') coin_name = self.default('COIN', 'Bitcoin')
network = self.default('NETWORK', 'mainnet') network = self.default('NETWORK', 'mainnet')
self.coin = Coin.lookup_coin_class(coin_name, network) self.coin = Coin.lookup_coin_class(coin_name, network)
self.db_dir = self.required('DB_DIRECTORY') self.db_dir = self.required('DB_DIRECTORY')
self.utxo_MB = self.integer('UTXO_MB', 1000) self.utxo_MB = self.integer('UTXO_MB', 1000)
self.hist_MB = self.integer('HIST_MB', 250) self.hist_MB = self.integer('HIST_MB', 250)
self.electrumx_rpc_port = self.integer('ELECTRUMX_RPC_PORT', 8000) self.host = self.default('HOST', 'localhost')
self.rpc_url = self.build_rpc_url() self.tcp_port = self.integer('TCP_PORT', None)
self.ssl_port = self.integer('SSL_PORT', None)
self.rpc_port = self.integer('RPC_PORT', 8000)
self.daemon_url = self.build_daemon_url()
self.max_subscriptions = self.integer('MAX_SUBSCRIPTIONS', 10000)
self.banner_file = self.default('BANNER_FILE', None)
# The electrum client takes the empty string as unspecified
self.donation_address = self.default('DONATION_ADDRESS', '')
def default(self, envvar, default): def default(self, envvar, default):
return environ.get(envvar, default) return environ.get(envvar, default)
@ -44,13 +50,13 @@ class Env(object):
raise self.Error('cannot convert envvar {} value {} to an integer' raise self.Error('cannot convert envvar {} value {} to an integer'
.format(envvar, value)) .format(envvar, value))
def build_rpc_url(self): def build_daemon_url(self):
rpc_url = environ.get('RPC_URL') daemon_url = environ.get('DAEMON_URL')
if not rpc_url: if not daemon_url:
rpc_username = self.required('RPC_USERNAME') username = self.required('DAEMON_USERNAME')
rpc_password = self.required('RPC_PASSWORD') password = self.required('DAEMON_PASSWORD')
rpc_host = self.required('RPC_HOST') host = self.required('DAEMON_HOST')
rpc_port = self.default('RPC_PORT', self.coin.DEFAULT_RPC_PORT) port = self.default('DAEMON_PORT', self.coin.DEFAULT_RPC_PORT)
rpc_url = ('http://{}:{}@{}:{}/' daemon_url = ('http://{}:{}@{}:{}/'
.format(rpc_username, rpc_password, rpc_host, rpc_port)) .format(username, password, host, port))
return rpc_url return daemon_url

206
server/protocol.py

@ -0,0 +1,206 @@
# See the file "LICENSE" for information about the copyright
# and warranty status of this software.
import asyncio
import codecs
import json
import traceback
from functools import partial
from lib.hash import hash_to_str
from lib.util import LoggedClass
from server.version import VERSION
class Error(Exception):
BAD_REQUEST = 1
INTERNAL_ERROR = 2
class JSONRPC(asyncio.Protocol, LoggedClass):
def __init__(self, controller):
super().__init__()
self.controller = controller
self.parts = []
def connection_made(self, transport):
self.transport = transport
peername = transport.get_extra_info('peername')
self.logger.info('connection from {}'.format(peername))
self.controller.add_session(self)
def connection_lost(self, exc):
self.logger.info('disconnected')
self.controller.remove_session(self)
def data_received(self, data):
while True:
npos = data.find(ord('\n'))
if npos == -1:
break
tail, data = data[:npos], data[npos + 1:]
parts = self.parts
self.parts = []
parts.append(tail)
self.decode_message(b''.join(parts))
if data:
self.parts.append(data)
def decode_message(self, message):
'''Message is a binary message.'''
try:
message = json.loads(message.decode())
except Exception as e:
self.logger.info('caught exception decoding message'.format(e))
return
job = self.request_handler(message)
self.controller.add_job(job)
async def request_handler(self, request):
'''Called asynchronously.'''
error = result = None
try:
result = await self.json_handler(request)
except Error as e:
error = {'code': e.args[0], 'message': e.args[1]}
except asyncio.CancelledError:
raise
except Exception as e:
# This should be considered a bug and fixed
traceback.print_exc()
error = {'code': Error.INTERNAL_ERROR, 'message': str(e)}
payload = {'id': request.get('id'), 'error': error, 'result': result}
try:
data = json.dumps(payload) + '\n'
except TypeError:
msg = 'cannot JSON encode response to request {}'.format(request)
self.logger.error(msg)
error = {'code': Error.INTERNAL_ERROR, 'message': msg}
payload = {'id': request.get('id'), 'error': error, 'result': None}
data = json.dumps(payload) + '\n'
self.transport.write(data.encode())
async def json_handler(self, request):
method = request.get('method')
handler = None
if isinstance(method, str):
handler_name = 'handle_{}'.format(method.replace('.', '_'))
handler = getattr(self, handler_name, None)
if not handler:
self.logger.info('unknown method: {}'.format(method))
raise Error(Error.BAD_REQUEST, 'unknown method: {}'.format(method))
params = request.get('params', [])
if not isinstance(params, list):
raise Error(Error.BAD_REQUEST, 'params should be an array')
return await handler(params)
class ElectrumX(JSONRPC):
def __init__(self, controller, env):
super().__init__(controller)
self.BC = controller.block_cache
self.db = controller.db
self.env = env
self.addresses = set()
self.subscribe_headers = False
def params_to_hash168(self, params):
if len(params) != 1:
raise Error(Error.BAD_REQUEST,
'params should contain a single address')
address = params[0]
try:
return self.env.coin.address_to_hash168(address)
except:
raise Error(Error.BAD_REQUEST,
'invalid address: {}'.format(address))
async def handle_blockchain_address_get_history(self, params):
hash168 = self.params_to_hash168(params)
history = [
{'tx_hash': hash_to_str(tx_hash), 'height': height}
for tx_hash, height in self.db.get_history(hash168, limit=None)
]
return history
async def handle_blockchain_address_subscribe(self, params):
hash168 = self.params_to_hash168(params)
status = self.controller.address_status(hash168)
return status.hex() if status else None
async def handle_blockchain_estimatefee(self, params):
result = await self.BC.send_single('estimatefee', params)
return result
async def handle_blockchain_headers_subscribe(self, params):
self.subscribe_headers = True
return self.db.get_current_header()
async def handle_blockchain_relayfee(self, params):
'''The minimum fee a low-priority tx must pay in order to be accepted
to this daemon's memory pool.
'''
net_info = await self.BC.send_single('getnetworkinfo')
return net_info['relayfee']
async def handle_server_banner(self, params):
'''Return the server banner.'''
banner = 'Welcome to Electrum!'
if self.env.banner_file:
try:
with codecs.open(self.env.banner_file, 'r', 'utf-8') as f:
banner = f.read()
except Exception as e:
self.logger.error('reading banner file {}: {}'
.format(self.env.banner_file, e))
return banner
async def handle_server_donation_address(self, params):
'''Return the donation address as a string.
If none is specified return the empty string.
'''
return self.env.donation_address
async def handle_server_peers_subscribe(self, params):
'''Returns the peer (ip, host, ports) tuples.
Despite the name electrum-server does not treat this as a
subscription.
'''
peers = self.controller.get_peers()
return tuple(peers.values())
async def handle_server_version(self, params):
'''Return the server version as a string.'''
return VERSION
class LocalRPC(JSONRPC):
async def handle_getinfo(self, params):
return {
'blocks': self.controller.db.height,
'peers': len(self.controller.get_peers()),
'sessions': len(self.controller.sessions),
'watched': sum(len(s.addresses) for s in self.controller.sessions
if isinstance(s, ElectrumX)),
'cached': 0,
}
async def handle_sessions(self, params):
return []
async def handle_numsessions(self, params):
return len(self.controller.sessions)
async def handle_peers(self, params):
return tuple(self.controller.get_peers().keys())
async def handle_numpeers(self, params):
return len(self.controller.get_peers())

60
server/rpc.py

@ -1,60 +0,0 @@
# See the file "LICENSE" for information about the copyright
# and warranty status of this software.
import logging
import traceback
from aiohttp import web
class ElectrumRPCServer(object):
'''ElectrumX's RPC server for localhost.'''
def __init__(self, server):
self.logger = logging.getLogger('RPCServer')
self.logger.setLevel(logging.INFO)
self.server = server
async def request_handler(self, request):
json_request = await request.json()
try:
err, result = await self.json_handler(json_request)
except Exception as e:
traceback.print_exc()
err, result = 1, 'caught exception: {}'.format(e)
id_ = request.get('id')
if err is None:
response = {
'id': id_,
'error': None,
'result': result,
}
else:
response = {
'id': id_,
'error': {'code': err, 'message': result},
'result': None,
}
return web.json_response(response)
async def json_handler(self, request):
method = request.get('method')
id_ = request.get('id')
params = request.get('params', [])
handler = getattr(self.server, 'handle_rpc_{}'.format(method), None)
if not handler:
return 1, 'unknown method "{}"'.format(method)
else:
return await handler(params)
def tasks(self, port):
self.logger.info('listening on port {:d}'.format(port))
app = web.Application()
app.router.add_post('/', self.request_handler)
host = '0.0.0.0'
loop = app.loop
handler = app.make_handler()
server = loop.create_server(handler, host, port)
return [server, app.startup()]

1
server/version.py

@ -0,0 +1 @@
VERSION = "ElectrumX 0.01"

10
server_main.py

@ -9,7 +9,7 @@ import os
import traceback import traceback
from server.env import Env from server.env import Env
from server.server import Server from server.controller import Controller
def main_loop(): def main_loop():
@ -22,15 +22,17 @@ def main_loop():
logging.info('switching current directory to {}'.format(env.db_dir)) logging.info('switching current directory to {}'.format(env.db_dir))
os.chdir(env.db_dir) os.chdir(env.db_dir)
server = Server(env)
tasks = server.async_tasks()
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
#loop.set_debug(True)
controller = Controller(env)
tasks = controller.start(loop)
try: try:
loop.run_until_complete(asyncio.gather(*tasks)) loop.run_until_complete(asyncio.gather(*tasks))
except asyncio.CancelledError: except asyncio.CancelledError:
logging.warning('task cancelled; asyncio event loop closing') logging.warning('task cancelled; asyncio event loop closing')
finally: finally:
controller.stop()
loop.close() loop.close()

Loading…
Cancel
Save