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()