#554 transactions GET/POST endpoints (#563)

* split up views per resource
add error cases to get/post transaction
update docs/test

* remove apiary from docstring

* PR review docs
This commit is contained in:
Dimitri De Jonghe 2016-08-19 13:56:08 +02:00 committed by Sylvain Bellemare
parent 7b767affc9
commit 87a57bae33
7 changed files with 238 additions and 57 deletions

View File

@ -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

View File

View File

@ -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

View File

@ -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']
})

View File

@ -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/<tx_id>')
@transaction_views.route('/transactions/<tx_id>')
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)

View File

@ -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 <python-server-api-examples.html>`_

View File

@ -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