diff --git a/.gitignore b/.gitignore index 1a9d961..46e5ff2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ *.pyc *.o *.so +config.json +tags +.*.sw* diff --git a/README.md b/README.md index 847b3bf..82e8421 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,11 @@ The darkwallet gateway is a daemon providing the following services to wallets: Generally the gateway tries to provide all services a wallet may need acting as a proxy to mask the user address so as to not compromise it in many services. +Install: +------------- + +see INSTALL file + Running: ----------- @@ -27,25 +32,25 @@ Client cheatlist: Fetching a block header ``` -$ curl http://localhost:8888/block/000000000000000145f738890dc703e7637b677f15e9a49ea2eeca6e6e3c5f51 +$ curl http://localhost:8888/rest/v1/block/000000000000000145f738890dc703e7637b677f15e9a49ea2eeca6e6e3c5f51 {"nonce": 2595258480, "timestamp": 1391031759, "version": 2, "prev_hash": "00000000000000012af08fe29312627aa6f74aa7f617925da4f4f3a572a95da0", "merkle": "088d6b08fbca5c9fb5c1970a7af5a17847d67635b80ca6f12bd982218e2a83ac", "bits": 419558700} ``` Fetching block transactions ``` -$ curl http://localhost:8888/block/000000000000000145f738890dc703e7637b677f15e9a49ea2eeca6e6e3c5f51/transactions +$ curl http://localhost:8888/rest/v1/block/000000000000000145f738890dc703e7637b677f15e9a49ea2eeca6e6e3c5f51/transactions {"transactions": ["0118256f73a29a2d6c06ea21fc48166ebf5acbcfaf57da3e173be7018e245338", "4424d7f653e29d731f95091d478816743c320fd7fa6a94f9bf8d4b2d7baf0975", .... ``` Fetching transaction ``` -$ curl http://localhost:8888/tx/5a002b39d70d0c3197afa1d2ae874083631f5a43cd4fe2b2cc35347d863f00f7 +$ curl http://localhost:8888/rest/v1/tx/5a002b39d70d0c3197afa1d2ae874083631f5a43cd4fe2b2cc35347d863f00f7 {"inputs": [{"previous_output": ["da03f16974423bf6425be37e7a6297587a35f117ce3b657e781eeff0098faed5", 0], "sequence": 4294967295, .... ``` address history ``` -$ curl http://localhost:8888/address/1dice3jkpTvevsohA4Np1yP4uKzG1SRLv +$ curl http://localhost:8888/rest/v1/address/1dice3jkpTvevsohA4Np1yP4uKzG1SRLv { "history" : [{"spend_hash": "cdf6ea4f4590fbc847855cf68af181f1398b8997081cf0cfbd14e0f2cf2808ea", "output_height": 228180, "spend_index": 0, "value": 1000000, .... ``` diff --git a/client/test.html b/client/test.html index 50e854a..ae23b62 100644 --- a/client/test.html +++ b/client/test.html @@ -14,7 +14,7 @@ write_to_screen("CONNECTED"); test(client); }; - var client = new GatewayClient("ws://obelisk.unsystem.net:8888/", handle_connect); + var client = new GatewayClient("ws://localhost:8888/", handle_connect); client.on_error = function(evt) { write_to_screen('ERROR: ' + evt.data); diff --git a/daemon/base58.py b/daemon/base58.py index 597b831..6a5f778 100644 --- a/daemon/base58.py +++ b/daemon/base58.py @@ -25,7 +25,7 @@ def b58encode(v): return (__b58chars[0]*nPad) + result -def b58decode(v, length): +def b58decode(v, length=None): """ decode v into a string of len bytes.""" long_value = 0L for (i, c) in enumerate(v[::-1]): @@ -47,4 +47,4 @@ def b58decode(v, length): if length is not None and len(result) != length: return None - return result \ No newline at end of file + return result diff --git a/daemon/gateway.py b/daemon/gateway.py index 96c075c..36ca169 100755 --- a/daemon/gateway.py +++ b/daemon/gateway.py @@ -1,15 +1,11 @@ #!/usr/bin/env python -import logging import tornado.options import tornado.web import tornado.websocket -import os.path import obelisk -import json import threading import code -from collections import defaultdict import config @@ -27,6 +23,7 @@ import rest_handlers import obelisk_handler +import querysocket_handler import jsonchan import broadcast import ticker @@ -43,110 +40,32 @@ def __init__(self, service): settings = dict(debug=True) settings.update(options.as_dict()) client = obelisk.ObeliskOfLightClient(service) + self.client = client self.obelisk_handler = obelisk_handler.ObeliskHandler(client) self.brc_handler = broadcast.BroadcastHandler() self.p2p = CryptoTransportLayer(config.get('p2p-port', 8889), config.get('external-ip', '127.0.0.1')) self.p2p.join_network(config.get('seeds', [])) self.json_chan_handler = jsonchan.JsonChanHandler(self.p2p) self.ticker_handler = ticker.TickerHandler() - + #websocket uri space handlers = [ - # /block/ - (r"/block/([^/]*)(?:/)?", rest_handlers.BlockHeaderHandler), - - # /block//transactions - (r"/block/([^/]*)/transactions(?:/)?", - rest_handlers.BlockTransactionsHandler), - - # /tx/ - (r"/tx(?:/)?", rest_handlers.TransactionPoolHandler), - - # /tx/ - (r"/tx/([^/]*)(?:/)?", rest_handlers.TransactionHandler), - - # /address/
- (r"/address/([^/]*)(?:/)?", rest_handlers.AddressHistoryHandler), - - # /height - (r"/height(?:/)?", rest_handlers.HeightHandler), + ## WebSocket Handler + (r"/", querysocket_handler.QuerySocketHandler) + ] - # / - (r"/", QuerySocketHandler) + # helloobelisk uri space + uri_space= r"/rest/v1/" + other_handlers = [ + (uri_space + r'net(?:/)?', rest_handlers.NetHandler), + (uri_space + r'height(?:/)?', rest_handlers.HeightHandler), + (uri_space + r'address/([^/]*)(?:/)?', rest_handlers.AddressHistoryHandler), + (uri_space + r'tx/([^/]*)(?:/)?', rest_handlers.TransactionHandler), + (uri_space + r'block/([^/]*)(?:/)?', rest_handlers.BlockHeaderHandler), + (uri_space + r"block/([^/]*)/transactions(?:/)?", rest_handlers.BlockTransactionsHandler), ] + all_handlers = other_handlers + handlers + tornado.web.Application.__init__(self, all_handlers, **settings) - tornado.web.Application.__init__(self, handlers, **settings) - -class QuerySocketHandler(tornado.websocket.WebSocketHandler): - - # Set of WebsocketHandler - listeners = set() - # Protects listeners - listen_lock = threading.Lock() - - def initialize(self): - self._obelisk_handler = self.application.obelisk_handler - self._brc_handler = self.application.brc_handler - self._json_chan_handler = self.application.json_chan_handler - self._ticker_handler = self.application.ticker_handler - self._subscriptions = defaultdict(dict) - self._connected = False - - def open(self): - logging.info("OPEN") - with QuerySocketHandler.listen_lock: - self.listeners.add(self) - self._connected = True - - def on_close(self): - logging.info("CLOSE") - disconnect_msg = {'command': 'disconnect_client', 'id': 0, 'params': []} - self._connected = False - self._obelisk_handler.handle_request(self, disconnect_msg) - self._json_chan_handler.handle_request(self, disconnect_msg) - with QuerySocketHandler.listen_lock: - self.listeners.remove(self) - - def _check_request(self, request): - return request.has_key("command") and request.has_key("id") and \ - request.has_key("params") and type(request["params"]) == list - - def on_message(self, message): - try: - request = json.loads(message) - except: - logging.error("Error decoding message: %s", message, exc_info=True) - - # Check request is correctly formed. - if not self._check_request(request): - logging.error("Malformed request: %s", request, exc_info=True) - return - # Try different handlers until one accepts request and - # processes it. - if self._json_chan_handler.handle_request(self, request): - return - if self._obelisk_handler.handle_request(self, request): - return - if self._brc_handler.handle_request(self, request): - return - if self._ticker_handler.handle_request(self, request): - return - logging.warning("Unhandled command. Dropping request: %s", - request, exc_info=True) - - def _send_response(self, response): - try: - self.write_message(json.dumps(response)) - except tornado.websocket.WebSocketClosedError: - self._connected = False - logging.warning("Dropping response to closed socket: %s", - response, exc_info=True) - - def queue_response(self, response): - try: - # calling write_message or the socket is not thread safe - ioloop.add_callback(self._send_response, response) - except: - logging.error("Error adding callback", exc_info=True) class DebugConsole(threading.Thread): @@ -170,5 +89,8 @@ def main(service): if __name__ == "__main__": service = config.get("obelisk-url", "tcp://127.0.0.1:9091") - main(service) + try: + main(service) + except KeyboardInterrupt: + reactor.stop() diff --git a/daemon/obelisk_handler.py b/daemon/obelisk_handler.py index e066bdd..a3bb30c 100644 --- a/daemon/obelisk_handler.py +++ b/daemon/obelisk_handler.py @@ -1,5 +1,6 @@ from twisted.internet import reactor +import base58 import logging import obelisk @@ -131,7 +132,7 @@ def call_method(self, method, params): def translate_arguments(self, params): if len(params) != 1 and len(params) != 2: - raise ValueError("Invalid parameter list length") + raise ValueError("Invalid parameter list length %s" % len(params)) address = params[0] if len(params) == 2: from_height = params[1] diff --git a/daemon/querysocket_handler.py b/daemon/querysocket_handler.py new file mode 100644 index 0000000..66a9d05 --- /dev/null +++ b/daemon/querysocket_handler.py @@ -0,0 +1,81 @@ +import tornado +import logging +import threading +from collections import defaultdict +import json + + +class QuerySocketHandler(tornado.websocket.WebSocketHandler): + + # Set of WebsocketHandler + listeners = set() + # Protects listeners + listen_lock = threading.Lock() + + def initialize(self): + self._obelisk_handler = self.application.obelisk_handler + self._brc_handler = self.application.brc_handler + self._json_chan_handler = self.application.json_chan_handler + self._ticker_handler = self.application.ticker_handler + self._subscriptions = defaultdict(dict) + self._connected = False + + def open(self): + logging.info("OPEN") + with QuerySocketHandler.listen_lock: + self.listeners.add(self) + self._connected = True + + def on_close(self): + logging.info("CLOSE") + disconnect_msg = {'command': 'disconnect_client', 'id': 0, 'params': []} + self._connected = False + self._obelisk_handler.handle_request(self, disconnect_msg) + self._json_chan_handler.handle_request(self, disconnect_msg) + with QuerySocketHandler.listen_lock: + self.listeners.remove(self) + + def _check_request(self, request): + return request.has_key("command") and request.has_key("id") and \ + request.has_key("params") and type(request["params"]) == list + + def on_message(self, message): + try: + request = json.loads(message) + except: + logging.error("Error decoding message: %s", message, exc_info=True) + return + + # Check request is correctly formed. + if not self._check_request(request): + logging.error("Malformed request: %s", request, exc_info=True) + return + # Try different handlers until one accepts request and + # processes it. + if self._json_chan_handler.handle_request(self, request): + return + if self._obelisk_handler.handle_request(self, request): + return + if self._brc_handler.handle_request(self, request): + return + if self._ticker_handler.handle_request(self, request): + return + logging.warning("Unhandled command. Dropping request: %s", + request, exc_info=True) + + def _send_response(self, response): + try: + self.write_message(json.dumps(response)) + except tornado.websocket.WebSocketClosedError: + self._connected = False + logging.warning("Dropping response to closed socket: %s", + response, exc_info=True) + + def queue_response(self, response): + ioloop = tornado.ioloop.IOLoop.current() + try: + # calling write_message or the socket is not thread safe + ioloop.add_callback(self._send_response, response) + except: + logging.error("Error adding callback", exc_info=True) + diff --git a/daemon/rest_handlers.py b/daemon/rest_handlers.py deleted file mode 100644 index 54505f3..0000000 --- a/daemon/rest_handlers.py +++ /dev/null @@ -1,129 +0,0 @@ -import tornado.web -import json -import base58 -import random - -from tornado.web import asynchronous, HTTPError - -def random_id_number(): - return random.randint(0, 2**32 - 1) - -# Implements the on_fetch method for all HTTP requests. -class BaseHTTPHandler(tornado.web.RequestHandler): - def on_fetch(self, response): - self.finish(json.dumps(response)) - - -class BlockHeaderHandler(tornado.web.RequestHandler): - @asynchronous - def get(self, blk_hash=None): - if blk_hash is None: - raise HTTPError(400, reason="No block hash") - - try: - blk_hash = blk_hash.decode("hex") - except ValueError: - raise HTTPError(400, reason="Invalid hash") - - request = { - "id": random_id_number(), - "command":"fetch_block_header", - "params": [blk_hash] - } - - self.application._obelisk_handler.handle_request(self, request) - - -class BlockTransactionsHandler(tornado.web.RequestHandler): - @asynchronous - def get(self, blk_hash=None): - if blk_hash is None: - raise HTTPError(400, reason="No block hash") - - try: - blk_hash = blk_hash.decode("hex") - except ValueError: - raise HTTPError(400, reason="Invalid hash") - - request = { - "id": random_id_number(), - "command":"fetch_block_transaction_hashes", - "params": [blk_hash] - } - - self.application._obelisk_handler.handle_request(self, request) - -class TransactionPoolHandler(tornado.web.RequestHandler): - @asynchronous - # Dump transaction pool to user - def get(self): - raise NotImplementedError - - def on_fetch(self, ec, pool): - raise NotImplementedError - - # Send tx if it is valid, - # validate if ?validate is in url... - def post(self): - raise NotImplementedError - - -class TransactionHandler(tornado.web.RequestHandler): - @asynchronous - def get(self, tx_hash=None): - if tx_hash is None: - raise HTTPError(400, reason="No block hash") - - try: - tx_hash = tx_hash.decode("hex") - except ValueError: - raise HTTPError(400, reason="Invalid hash") - - request = { - "id": random_id_number(), - "command":"fetch_transaction", - "params": [tx_hash] - } - - self.application._obelisk_handler.handle_request(self, request) - -class AddressHistoryHandler(tornado.web.RequestHandler): - @asynchronous - def get(self, address=None): - if address is None: - raise HTTPError(400, reason="No address") - - try: - from_height = long(self.get_argument("from_height", 0)) - except: - raise HTTPError(400) - - address_decoded = base58.b58decode(address) - address_version = address_decoded[0] - address_hash = address_decoded[1:21] - - request = { - "id": random_id_number(), - "command":"fetch_history", - "params": [address_version, address_hash, from_height] - } - - self.application._obelisk_handler.handle_request(self, request) - - -class BaseHTTPHandler(tornado.web.RequestHandler): - def on_fetch(self, response): - self.finish(response) - - -class HeightHandler(BaseHTTPHandler): - @asynchronous - def get(self): - request = { - "id": random_id_number(), - "command":"fetch_last_height", - "params": None - } - - self.application._obelisk_handler.handle_request(self, request) - diff --git a/daemon/rest_handlers/__init__.py b/daemon/rest_handlers/__init__.py new file mode 100644 index 0000000..61ce840 --- /dev/null +++ b/daemon/rest_handlers/__init__.py @@ -0,0 +1,2 @@ +from .rest_handlers import * + diff --git a/daemon/rest_handlers/rest_handlers.py b/daemon/rest_handlers/rest_handlers.py new file mode 100644 index 0000000..b8c0954 --- /dev/null +++ b/daemon/rest_handlers/rest_handlers.py @@ -0,0 +1,197 @@ +import tornado.web +import json +import random +import logging +from tornado.web import asynchronous, HTTPError +import obelisk +from obelisk import bitcoin + +def random_id_number(): + return random.randint(0, 2**32 - 1) + +class BaseHTTPHandler(tornado.web.RequestHandler): + def send_response(self, response): + self._response(response) + + def _response(self, response): + self.write(json.dumps(response)) + self.finish() + + def success_response(self, data): + return { + 'status':'success', + 'data': data + } + + def fail_response(self, data): + return { + 'status':'fail', + 'data': data + } + + def error_response(self, data): + return { + 'status':'error', + 'data':data + } + +class BlockHeaderHandler(BaseHTTPHandler): + @asynchronous + def get(self, blk_hash=None): + if blk_hash is None: + response = self.error_response("no block hash") + self.send_response(response) + + try: + blk_hash = blk_hash.decode("hex") + except ValueError: + response = self.error_response("Invalid block") + self.send_response(response) + + self.application.client.fetch_block_header(blk_hash, self._callback_response) + + + def _callback_response(self, ec, header_bin): + header = obelisk.serialize.deser_block_header(header_bin) + pbh = header.previous_block_hash.encode("hex") + data = { + 'header_block': { + 'hash': obelisk.serialize.hash_block_header(header).encode("hex"), + 'version': header.version, + 'previous_block_hash': pbh.decode("hex")[::-1].encode("hex"), + 'merkle': header.merkle.encode("hex"), + 'timestamp': header.timestamp, + 'bits': header.bits, + 'nonce':header.nonce, + } + } + response_dict = self.success_response(data) + self.send_response(response_dict) + +class BlockTransactionsHandler(BaseHTTPHandler): + @asynchronous + def get(self, blk_hash=None): + if blk_hash is None: + response = self.error_response("no block hash") + self.send_response(response) + + try: + blk_hash = blk_hash.decode("hex") + except ValueError: + response = self.error_response("Invalid block") + self.send_response(response) + + self.application.client.fetch_block_transaction_hashes(blk_hash, self._callback_response) + + def _callback_response(self, ec, list_hash): + trans = [] + for row in list_hash: + tx_hex = row.encode("hex") + trans.append(tx_hex) + self.send_response(trans) + +class TransactionHandler(BaseHTTPHandler): + @asynchronous + def get(self, tx_hash=None): + if tx_hash is None: + response = self.fail_response("missing tx_hash") + self.send_response(response) + try: + tx_hash = tx_hash.decode("hex") + except ValueError: + response = self.fail_response("invalid tx_hash") + self.send_response(response) + logging.info("transaction %s", tx_hash) + self.application.client.fetch_transaction( tx_hash, self._callback_response) + + def _callback_response(self, ec, tx): + transaction = tx.encode("hex") + tx_ = obelisk.bitcoin.Transaction(transaction) + data = { + 'transaction':{ + 'hash': tx_.hash(), + 'deserialize': tx_.deserialize(), + 'tx': tx_.as_dict() + } + } + response = self.success_response(data) + self.send_response(response) + +class AddressHistoryHandler(BaseHTTPHandler): + @asynchronous + def get(self, address=None): + if address is None: + raise HTTPError(400, reason="No address") + + self.address = address + self.application.client.fetch_history(address, self._callback_response) + + def _callback_response(self,ec,history): + address = {} + total_balance = 0 + total_balance += sum(row[3] for row in history + if row[-1] != obelisk.MAX_UINT32) + transactions = [] + for row in history: + o_hash, o_index, o_height, value, s_hash, s_index, s_height = row + def check_none( hash_): + if hash_ is None: + return None + else: + return hash_.encode("hex") + + transaction = { + 'output_hash': check_none(o_hash),'output_index': o_index,'output_height':o_height, + 'value': value, + 'spend_hash':check_none(s_hash), 'spend_index': s_index, 'spend_height': s_height, + } + transactions.append(transaction) + + address.update({'total_balance': total_balance, + 'address':self.address, 'transactions': transactions}) + data = {'address': address} + if not ec: + response = self.success_response(data) + else: + response = self.error_response(history) + self.send_response(response) + +class HeightHandler(BlockHeaderHandler, BaseHTTPHandler): + @asynchronous + def get(self): + self.application.client.fetch_last_height(self._before_callback_response) + + def _before_callback_response(self, ec, height): + self.height = height + self.application.client.fetch_block_header(height, self._callback_response) + + def _callback_response(self, ec, header_bin): + header = obelisk.serialize.deser_block_header(header_bin) + pbh = header.previous_block_hash.encode("hex") + data = { 'last_height': self.height, + 'last_header_block': { + 'hash': obelisk.serialize.hash_block_header(header).encode("hex"), + 'version': header.version, + 'previous_block_hash': pbh.decode("hex")[::-1].encode("hex"), + 'merkle': header.merkle.encode("hex"), + 'timestamp': header.timestamp, + 'bits': header.bits, + 'nonce':header.nonce, + } + } + response_dict = self.success_response(data) + self.send_response(response_dict) + + +class NetHandler( BaseHTTPHandler): + def get(self): + chain = obelisk.config.chain + if chain.magic_bytes == 0x00: + net = "main" + else: + net = "testnet" + response = { + 'chain': net, + } + + self.send_response(response) diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..303dc2e --- /dev/null +++ b/tests.py @@ -0,0 +1,23 @@ +import requests +import unittest + +class TestNet(unittest.TestCase): + # run gw on testnet_mode + + url = "http://localhost:8888/rest/v1/" + address = "mo6Qh6iEhzHnt1R8jRQwagaBeXFv5eX2W4" + + def test_testnet(self): + net = requests.get(self.url + "net/").json() + self.assertTrue(net.get("chain") == "testnet") + + def test_address(self): + res = requests.get(self.url + "address/%s" % self.address).json() + self.assertTrue(res.get("status") == "success") + self.assertTrue("address" in res["data"]) + + + + +if __name__ == '__main__': + unittest.main()