diff --git a/bigchaindb/web/server.py b/bigchaindb/web/server.py index 2bc1e92f..43c4d6c1 100644 --- a/bigchaindb/web/server.py +++ b/bigchaindb/web/server.py @@ -11,7 +11,9 @@ import gunicorn.app.base from bigchaindb import util from bigchaindb import Bigchain -from bigchaindb.web import views +from bigchaindb.web.views.info import info_views +from bigchaindb.web.views.transactions import transaction_views + from bigchaindb.monitor import Monitor @@ -62,8 +64,8 @@ def create_app(settings): app.config['bigchain_pool'] = util.pool(Bigchain, size=settings.get('threads', 4)) app.config['monitor'] = Monitor() - app.register_blueprint(views.info_views, url_prefix='/') - app.register_blueprint(views.basic_views, url_prefix='/api/v1') + app.register_blueprint(info_views, url_prefix='/') + app.register_blueprint(transaction_views, url_prefix='/api/v1') return app diff --git a/bigchaindb/web/views/__init__.py b/bigchaindb/web/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bigchaindb/web/views/base.py b/bigchaindb/web/views/base.py new file mode 100644 index 00000000..82fdc418 --- /dev/null +++ b/bigchaindb/web/views/base.py @@ -0,0 +1,15 @@ +from flask import jsonify + + +def make_error(status_code, message=None): + + if status_code == 404 and message is None: + message = 'Not found' + + response = jsonify({ + 'status': status_code, + 'message': message + }) + response.status_code = status_code + return response + diff --git a/bigchaindb/web/views/info.py b/bigchaindb/web/views/info.py new file mode 100644 index 00000000..a6602657 --- /dev/null +++ b/bigchaindb/web/views/info.py @@ -0,0 +1,26 @@ +"""This module provides the blueprint for some basic API endpoints. + +For more information please refer to the documentation on ReadTheDocs: + - https://bigchaindb.readthedocs.io/en/latest/drivers-clients/http-client-server-api.html +""" + +import flask +from flask import Blueprint + +import bigchaindb +from bigchaindb import version + + +info_views = Blueprint('info_views', __name__) + + +@info_views.route('/') +def home(): + return flask.jsonify({ + 'software': 'BigchainDB', + 'version': version.__version__, + 'public_key': bigchaindb.config['keypair']['public'], + 'keyring': bigchaindb.config['keyring'], + 'api_endpoint': bigchaindb.config['api_endpoint'] + }) + diff --git a/bigchaindb/web/views.py b/bigchaindb/web/views/transactions.py similarity index 68% rename from bigchaindb/web/views.py rename to bigchaindb/web/views/transactions.py index 6ca91670..10603ec1 100644 --- a/bigchaindb/web/views.py +++ b/bigchaindb/web/views/transactions.py @@ -1,24 +1,23 @@ """This module provides the blueprint for some basic API endpoints. -For more information please refer to the documentation in Apiary: - - http://docs.bigchaindb.apiary.io/ +For more information please refer to the documentation on ReadTheDocs: + - https://bigchaindb.readthedocs.io/en/latest/drivers-clients/http-client-server-api.html """ import flask -from flask import abort, current_app, request, Blueprint +from flask import current_app, request, Blueprint import bigchaindb -from bigchaindb import util, version +from bigchaindb import util +from bigchaindb.web.views.base import make_error - -info_views = Blueprint('info_views', __name__) -basic_views = Blueprint('basic_views', __name__) +transaction_views = Blueprint('transaction_views', __name__) # Unfortunately I cannot find a reference to this decorator. # This answer on SO is quite useful tho: # - http://stackoverflow.com/a/13432373/597097 -@basic_views.record +@transaction_views.record def record(state): """This function checks if the blueprint can be initialized with the provided state.""" @@ -35,18 +34,8 @@ def record(state): 'a monitor instance to record system ' 'performance.') -@info_views.route('/') -def home(): - return flask.jsonify({ - 'software': 'BigchainDB', - 'version': version.__version__, - 'public_key': bigchaindb.config['keypair']['public'], - 'keyring': bigchaindb.config['keyring'], - 'api_endpoint': bigchaindb.config['api_endpoint'] - }) - -@basic_views.route('/transactions/') +@transaction_views.route('/transactions/') def get_transaction(tx_id): """API endpoint to get details about a transaction. @@ -63,12 +52,12 @@ def get_transaction(tx_id): tx = bigchain.get_transaction(tx_id) if not tx: - abort(404) + return make_error(404) return flask.jsonify(**tx) -@basic_views.route('/transactions/', methods=['POST']) +@transaction_views.route('/transactions/', methods=['POST']) def create_transaction(): """API endpoint to push transactions to the Federation. @@ -78,8 +67,6 @@ def create_transaction(): pool = current_app.config['bigchain_pool'] monitor = current_app.config['monitor'] - val = {} - # `force` will try to format the body of the POST request even if the `content-type` header is not # set to `application/json` tx = request.get_json(force=True) @@ -89,11 +76,11 @@ def create_transaction(): tx = util.transform_create(tx) tx = bigchain.consensus.sign_transaction(tx, private_key=bigchain.me_private) - if not bigchain.consensus.validate_fulfillments(tx): - val['error'] = 'Invalid transaction fulfillments' + if not bigchain.is_valid_transaction(tx): + return make_error(400, 'Invalid transaction') with monitor.timer('write_transaction', rate=bigchaindb.config['statsd']['rate']): - val = bigchain.write_transaction(tx) + bigchain.write_transaction(tx) return flask.jsonify(**tx) diff --git a/docs/source/drivers-clients/http-client-server-api.rst b/docs/source/drivers-clients/http-client-server-api.rst index e0b55260..1da816ae 100644 --- a/docs/source/drivers-clients/http-client-server-api.rst +++ b/docs/source/drivers-clients/http-client-server-api.rst @@ -13,17 +13,21 @@ There are other configuration settings related to the web server (serving the HT The HTTP API currently exposes two endpoints, one to get information about a specific transaction, and one to push a new transaction to the BigchainDB cluster. -.. http:get:: /transactions/(tx_id) +.. http:get:: /transactions/{tx_id} - The transaction with the transaction ID `tx_id`. + Get the transaction with the ID ``tx_id``. + + This endpoint returns only a transaction from a ``VALID`` or ``UNDECIDED`` block on ``bigchain``, if exists. + + :param tx_id: transaction ID + :type tx_id: hex string **Example request**: .. sourcecode:: http - GET /transactions/96480ce68912aa39a54766ac16334a835fbf777039670352ff967bf6d65bf4f7 HTTP/1.1 + GET /transactions/7ad5a4b83bc8c70c4fd7420ff3c60693ab8e6d0e3124378ca69ed5acd2578792 HTTP/1.1 Host: example.com - TODO: Other headers? **Example response**: @@ -31,30 +35,56 @@ The HTTP API currently exposes two endpoints, one to get information about a spe HTTP/1.1 200 OK Content-Type: application/json - TODO: Other headers? - - {'id': '96480ce68912aa39a54766ac16334a835fbf777039670352ff967bf6d65bf4f7', - 'transaction': {'conditions': [{'cid': 0, - 'condition': {'details': {'bitmask': 32, - 'public_key': 'FoWUUY6kK7QhgCsgVrV2vpDWfW43mq5ewb16Uh7FBbSF', - 'signature': None, - 'type': 'fulfillment', - 'type_id': 4}, - 'uri': 'cc:4:20:2-2pA2qKr2i-GM6REdqJCLEL_CEWpy-5iQky7YgRZTA:96'}, - 'new_owners': ['FoWUUY6kK7QhgCsgVrV2vpDWfW43mq5ewb16Uh7FBbSF']}], - 'data': {'payload': None, 'uuid': 'f14dc5a6-510e-4307-89c6-aec42af8a1ae'}, - 'fulfillments': [{'current_owners': ['Ftat68WVLsPxVFLz2Rh2Sbwrrt51uFE3UpjkxY73vGKZ'], - 'fid': 0, - 'fulfillment': 'cf:4:3TqMI1ZFolraqHWADT6nIvUUt4HOwqdr0_-yj5Cglbg1V5qQV2CF2Yup1l6fQH2uhLGGFo9uHhZ6HNv9lssiD0ZaG88Bg_MTkz6xg2SW2Cw_YgpM-CyESVT404g54ZsK', - 'input': None}], - 'operation': 'CREATE', - 'timestamp': '1468494923'}, - 'version': 1} + + { + "id":"7ad5a4b83bc8c70c4fd7420ff3c60693ab8e6d0e3124378ca69ed5acd2578792", + "transaction":{ + "conditions":[ + { + "cid":0, + "condition":{ + "details":{ + "bitmask":32, + "public_key":"CwA8s2QYQBfNz4WvjEwmJi83zYr7JhxRhidx6uZ5KBVd", + "signature":null, + "type":"fulfillment", + "type_id":4 + }, + "uri":"cc:4:20:sVA_3p8gvl8yRFNTomqm6MaavKewka6dGYcFAuPrRXQ:96" + }, + "new_owners":[ + "CwA8s2QYQBfNz4WvjEwmJi83zYr7JhxRhidx6uZ5KBVd" + ] + } + ], + "data":{ + "payload":null, + "uuid":"a9999d69-6cde-4b80-819d-ed57f6abe257" + }, + "fulfillments":[ + { + "current_owners":[ + "JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE" + ], + "fid":0, + "fulfillment":"cf:4:__Y_Um6H73iwPe6ejWXEw930SQhqVGjtAHTXilPp0P01vE_Cx6zs3GJVoO1jhPL18C94PIVkLTGMUB2aKC9qsbIb3w8ejpOf0_I3OCuTbPdkd6r2lKMeVftMyMxkeWoM", + "input":{ + "cid":0, + "txid":"598ce4e9a29837a1c6fc337ee4a41b61c20ad25d01646754c825b1116abd8761" + } + } + ], + "operation":"TRANSFER", + "timestamp":"1471423869", + "version":1 + } + } :statuscode 200: A transaction with that ID was found. :statuscode 404: A transaction with that ID was not found. + .. http:post:: /transactions/ Push a new transaction. @@ -66,9 +96,50 @@ The HTTP API currently exposes two endpoints, one to get information about a spe POST /transactions/ HTTP/1.1 Host: example.com Content-Type: application/json - TODO: Other headers? - (TODO) Insert example request body here + { + "id":"7ad5a4b83bc8c70c4fd7420ff3c60693ab8e6d0e3124378ca69ed5acd2578792", + "transaction":{ + "conditions":[ + { + "cid":0, + "condition":{ + "details":{ + "bitmask":32, + "public_key":"CwA8s2QYQBfNz4WvjEwmJi83zYr7JhxRhidx6uZ5KBVd", + "signature":null, + "type":"fulfillment", + "type_id":4 + }, + "uri":"cc:4:20:sVA_3p8gvl8yRFNTomqm6MaavKewka6dGYcFAuPrRXQ:96" + }, + "new_owners":[ + "CwA8s2QYQBfNz4WvjEwmJi83zYr7JhxRhidx6uZ5KBVd" + ] + } + ], + "data":{ + "payload":null, + "uuid":"a9999d69-6cde-4b80-819d-ed57f6abe257" + }, + "fulfillments":[ + { + "current_owners":[ + "JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE" + ], + "fid":0, + "fulfillment":"cf:4:__Y_Um6H73iwPe6ejWXEw930SQhqVGjtAHTXilPp0P01vE_Cx6zs3GJVoO1jhPL18C94PIVkLTGMUB2aKC9qsbIb3w8ejpOf0_I3OCuTbPdkd6r2lKMeVftMyMxkeWoM", + "input":{ + "cid":0, + "txid":"598ce4e9a29837a1c6fc337ee4a41b61c20ad25d01646754c825b1116abd8761" + } + } + ], + "operation":"TRANSFER", + "timestamp":"1471423869", + "version":1 + } + } **Example response**: @@ -76,10 +147,78 @@ The HTTP API currently exposes two endpoints, one to get information about a spe HTTP/1.1 201 Created Content-Type: application/json - TODO: Other headers? - (TODO) Insert example response body here + { + "assignee":"4XYfCbabAWVUCbjTmRTFEu2sc3dFEdkse4r6X498B1s8", + "id":"7ad5a4b83bc8c70c4fd7420ff3c60693ab8e6d0e3124378ca69ed5acd2578792", + "transaction":{ + "conditions":[ + { + "cid":0, + "condition":{ + "details":{ + "bitmask":32, + "public_key":"CwA8s2QYQBfNz4WvjEwmJi83zYr7JhxRhidx6uZ5KBVd", + "signature":null, + "type":"fulfillment", + "type_id":4 + }, + "uri":"cc:4:20:sVA_3p8gvl8yRFNTomqm6MaavKewka6dGYcFAuPrRXQ:96" + }, + "new_owners":[ + "CwA8s2QYQBfNz4WvjEwmJi83zYr7JhxRhidx6uZ5KBVd" + ] + } + ], + "data":{ + "payload":null, + "uuid":"a9999d69-6cde-4b80-819d-ed57f6abe257" + }, + "fulfillments":[ + { + "current_owners":[ + "JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE" + ], + "fid":0, + "fulfillment":"cf:4:__Y_Um6H73iwPe6ejWXEw930SQhqVGjtAHTXilPp0P01vE_Cx6zs3GJVoO1jhPL18C94PIVkLTGMUB2aKC9qsbIb3w8ejpOf0_I3OCuTbPdkd6r2lKMeVftMyMxkeWoM", + "input":{ + "cid":0, + "txid":"598ce4e9a29837a1c6fc337ee4a41b61c20ad25d01646754c825b1116abd8761" + } + } + ], + "operation":"TRANSFER", + "timestamp":"1471423869", + "version":1 + } + } :statuscode 201: A new transaction was created. + :statuscode 400: The transaction was invalid and not created. -(TODO) What's the response status code if the POST fails? + **Disclaimer** + + ``CREATE`` transactions are treated differently from ``TRANSFER`` assets. + The reason is that a ``CREATE`` transaction needs to be signed by a federation node and not by the client. + + The following python snippet in a client can be used to generate ``CREATE`` transactions before they can be pushed to the API server: + + .. code-block:: python + + from bigchaindb import util + tx = util.create_and_sign_tx(my_privkey, my_pubkey, my_pubkey, None, 'CREATE') + + When POSTing ``tx`` to the API, the ``CREATE`` transaction will be signed by a federation node. + + A ``TRANSFER`` transaction, that takes an existing input transaction to change ownership can be generated in multiple ways: + + .. code-block:: python + + from bigchaindb import util, Bigchain + tx = util.create_and_sign_tx(my_privkey, my_pubkey, other_pubkey, input_tx, 'TRANSFER') + # or + b = Bigchain() + tx_unsigned = b.create_transaction(my_pubkey, other_pubkey, input_tx, 'TRANSFER') + tx = b.sign_transaction(tx_unsigned, my_privkey) + + More information on generating transactions can be found in the `Python server API examples `_ \ No newline at end of file diff --git a/tests/web/test_basic_views.py b/tests/web/test_basic_views.py index dd460139..2ed05caf 100644 --- a/tests/web/test_basic_views.py +++ b/tests/web/test_basic_views.py @@ -14,6 +14,7 @@ def test_get_transaction_endpoint(b, client, user_vk): tx = b.get_transaction(input_tx['txid']) res = client.get(TX_ENDPOINT + input_tx['txid']) assert tx == res.json + assert res.status_code == 200 @pytest.mark.usefixtures('inputs') @@ -50,3 +51,14 @@ def test_post_transfer_transaction_endpoint(b, client, user_vk, user_sk): assert res.json['transaction']['fulfillments'][0]['current_owners'][0] == user_vk assert res.json['transaction']['conditions'][0]['new_owners'][0] == to_keypair[1] + +@pytest.mark.usefixtures('inputs') +def test_post_invalid_transfer_transaction_returns_400(b, client, user_vk, user_sk): + to_keypair = crypto.generate_key_pair() + input_valid = b.get_owned_ids(user_vk).pop() + transfer = b.create_transaction(user_vk, to_keypair[0], input_valid, 'TRANSFER') + # transfer is not signed + res = client.post(TX_ENDPOINT, data=json.dumps(transfer)) + + assert res.status_code == 400 +