Merge pull request #1996 from bigchaindb/1995-mode-API

add mode parameter to transaction endpoint, fixes #1995
This commit is contained in:
vrde 2018-01-29 12:00:32 +01:00 committed by GitHub
commit c156d0bfe8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 121 additions and 11 deletions

View File

@ -19,15 +19,21 @@ logger = logging.getLogger(__name__)
TENDERMINT_HOST = getenv('TENDERMINT_HOST', 'localhost')
TENDERMINT_PORT = getenv('TENDERMINT_PORT', '46657')
ENDPOINT = 'http://{}:{}/'.format(TENDERMINT_HOST, TENDERMINT_PORT)
MODE_LIST = ('broadcast_tx_async',
'broadcast_tx_sync',
'broadcast_tx_commit')
class BigchainDB(Bigchain):
def post_transaction(self, transaction):
def post_transaction(self, transaction, mode):
"""Submit a valid transaction to the mempool."""
if not mode or mode not in MODE_LIST:
raise ValidationError(('Mode must be one of the following {}.')
.format(', '.join(MODE_LIST)))
payload = {
'method': 'broadcast_tx_async',
'method': mode,
'jsonrpc': '2.0',
'params': [encode_transaction(transaction.to_dict())],
'id': str(uuid4())
@ -35,11 +41,10 @@ class BigchainDB(Bigchain):
# TODO: handle connection errors!
requests.post(ENDPOINT, json=payload)
def write_transaction(self, transaction):
def write_transaction(self, transaction, mode):
# This method offers backward compatibility with the Web API.
"""Submit a valid transaction to the mempool."""
self.post_transaction(transaction)
self.post_transaction(transaction, mode)
def store_transaction(self, transaction):
"""Store a valid transaction to the transactions collection."""

View File

@ -29,4 +29,14 @@ def valid_operation(op):
return 'CREATE'
if op == 'TRANSFER':
return 'TRANSFER'
raise ValueError('Operation must be "CREATE" or "TRANSFER')
raise ValueError('Operation must be "CREATE" or "TRANSFER"')
def valid_mode(mode):
if mode == 'async':
return 'broadcast_tx_async'
if mode == 'sync':
return 'broadcast_tx_sync'
if mode == 'commit':
return 'broadcast_tx_commit'
raise ValueError('Mode must be "async", "sync" or "commit"')

View File

@ -55,6 +55,12 @@ class TransactionListApi(Resource):
Return:
A ``dict`` containing the data about the transaction.
"""
parser = reqparse.RequestParser()
parser.add_argument('mode', type=parameters.valid_mode,
default='broadcast_tx_async')
args = parser.parse_args()
mode = str(args['mode'])
pool = current_app.config['bigchain_pool']
# `force` will try to format the body of the POST request even if the
@ -85,7 +91,7 @@ class TransactionListApi(Resource):
'Invalid transaction ({}): {}'.format(type(e).__name__, e)
)
else:
bigchain.write_transaction(tx_obj)
bigchain.write_transaction(tx_obj, mode)
response = jsonify(tx)
response.status_code = 202

View File

@ -58,7 +58,7 @@ Content-Type: application/json
"""
TPLS['post-tx-request'] = """\
POST /api/v1/transactions/ HTTP/1.1
POST /api/v1/transactions?mode=async HTTP/1.1
Host: example.com
Content-Type: application/json

View File

@ -124,11 +124,20 @@ Transactions
:statuscode 400: The request wasn't understood by the server, e.g. the ``asset_id`` querystring was not included in the request.
.. http:post:: /api/v1/transactions
.. http:post:: /api/v1/transactions?mode={mode}
Push a new transaction.
Tendermint offers a `broadcast API
<http://tendermint.readthedocs.io/projects/tools/en/master/using-tendermint.html#broadcast-api>`_ with three different modes to post transactions.
By setting the mode, a new transaction can be pushed with a different mode than the default. The default mode is ``async``, which
will return immediately and not wait to see if the transaction is valid. The ``sync`` mode will return after the transaction is validated, while ``commit``
returns after the transaction is committed to a block.
.. note::
This option is only available when using BigchainDB with Tendermint.
.. note::
The posted `transaction
<https://docs.bigchaindb.com/projects/server/en/latest/data-models/transaction-model.html>`_
should be structurally valid and not spending an already spent output.
@ -137,6 +146,8 @@ Transactions
<https://docs.bigchaindb.com/projects/py-driver/en/latest/index.html>`_
to build a valid transaction.
:query string mode: (Optional) One of the three supported modes to send a transaction: ``async``, ``sync``, ``commit``.
**Example request**:
.. literalinclude:: http-samples/post-tx-request.http
@ -162,6 +173,11 @@ Transactions
:statuscode 400: The transaction was malformed and not accepted in the ``BACKLOG``.
.. http:post:: /api/v1/transactions
This endpoint (without any parameters) will push a new transaction. If BigchainDB is used with Tendermint, the default mode ``async`` is used.
Transaction Outputs
-------------------

View File

@ -76,10 +76,45 @@ def test_write_and_post_transaction(mock_post, b):
.sign([alice.private_key]).to_dict()
tx = b.validate_transaction(tx)
b.write_transaction(tx)
b.write_transaction(tx, 'broadcast_tx_async')
assert mock_post.called
args, kwargs = mock_post.call_args
assert 'broadcast_tx_async' == kwargs['json']['method']
encoded_tx = [encode_transaction(tx.to_dict())]
assert encoded_tx == kwargs['json']['params']
@patch('requests.post')
@pytest.mark.parametrize('mode', [
'broadcast_tx_async',
'broadcast_tx_sync',
'broadcast_tx_commit'
])
def test_post_transaction_valid_modes(mock_post, b, mode):
from bigchaindb.models import Transaction
from bigchaindb.common.crypto import generate_key_pair
alice = generate_key_pair()
tx = Transaction.create([alice.public_key],
[([alice.public_key], 1)],
asset=None) \
.sign([alice.private_key]).to_dict()
tx = b.validate_transaction(tx)
b.write_transaction(tx, mode)
args, kwargs = mock_post.call_args
assert mode == kwargs['json']['method']
def test_post_transaction_invalid_mode(b):
from bigchaindb.models import Transaction
from bigchaindb.common.crypto import generate_key_pair
from bigchaindb.common.exceptions import ValidationError
alice = generate_key_pair()
tx = Transaction.create([alice.public_key],
[([alice.public_key], 1)],
asset=None) \
.sign([alice.private_key]).to_dict()
tx = b.validate_transaction(tx)
with pytest.raises(ValidationError):
b.write_transaction(tx, 'nope')

View File

@ -410,3 +410,41 @@ def test_return_only_valid_transaction(client):
get_transaction_patched(Bigchain.TX_IN_BACKLOG)):
url = '{}{}'.format(TX_ENDPOINT, '123')
assert client.get(url).status_code == 404
@pytest.mark.tendermint
@patch('requests.post')
@pytest.mark.parametrize('mode', [
('', 'broadcast_tx_async'),
('?mode=async', 'broadcast_tx_async'),
('?mode=sync', 'broadcast_tx_sync'),
('?mode=commit', 'broadcast_tx_commit'),
])
def test_post_transaction_valid_modes(mock_post, client, mode):
from bigchaindb.models import Transaction
from bigchaindb.common.crypto import generate_key_pair
alice = generate_key_pair()
tx = Transaction.create([alice.public_key],
[([alice.public_key], 1)],
asset=None) \
.sign([alice.private_key])
mode_endpoint = TX_ENDPOINT + mode[0]
client.post(mode_endpoint, data=json.dumps(tx.to_dict()))
args, kwargs = mock_post.call_args
assert mode[1] == kwargs['json']['method']
@pytest.mark.tendermint
def test_post_transaction_invalid_mode(client):
from bigchaindb.models import Transaction
from bigchaindb.common.crypto import generate_key_pair
alice = generate_key_pair()
tx = Transaction.create([alice.public_key],
[([alice.public_key], 1)],
asset=None) \
.sign([alice.private_key])
mode_endpoint = TX_ENDPOINT + '?mode=nope'
response = client.post(mode_endpoint, data=json.dumps(tx.to_dict()))
assert '400 BAD REQUEST' in response.status
assert 'Mode must be "async", "sync" or "commit"' ==\
json.loads(response.data.decode('utf8'))['message']['mode']