Neil Booth
8 years ago
18 changed files with 412 additions and 144 deletions
@ -0,0 +1 @@ |
|||
8332 |
@ -1 +1 @@ |
|||
8332 |
|||
8000 |
|||
|
@ -0,0 +1 @@ |
|||
50002 |
@ -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()) |
@ -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()] |
@ -0,0 +1 @@ |
|||
VERSION = "ElectrumX 0.01" |
Loading…
Reference in new issue