diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index 35ae16bf..0e6d84f2 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -159,7 +159,7 @@ class TransactionLink(object): def __eq__(self, other): # TODO: If `other !== TransactionLink` return `False` - return self.to_dict() == self.to_dict() + return self.to_dict() == other.to_dict() @classmethod def from_dict(cls, link): diff --git a/bigchaindb/core.py b/bigchaindb/core.py index f9c96bed..871c3707 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -373,8 +373,9 @@ class Bigchain(object): else: return None - def get_owned_ids(self, owner): - """Retrieve a list of ``txid`` s that can be used as inputs. + def get_outputs(self, owner): + """Retrieve a list of links to transaction outputs for a given public + key. Args: owner (str): base58 encoded public key. @@ -383,10 +384,9 @@ class Bigchain(object): :obj:`list` of TransactionLink: list of ``txid`` s and ``output`` s pointing to another transaction's condition """ - # get all transactions in which owner is in the `owners_after` list response = backend.query.get_owned_ids(self.connection, owner) - owned = [] + links = [] for tx in response: # disregard transactions from invalid blocks @@ -411,11 +411,30 @@ class Bigchain(object): # subfulfillment for `owner` if utils.condition_details_has_owner(output['condition']['details'], owner): tx_link = TransactionLink(tx['id'], index) - # check if input was already spent - if not self.get_spent(tx_link.txid, tx_link.output): - owned.append(tx_link) + links.append(tx_link) + return links - return owned + def get_owned_ids(self, owner): + """Retrieve a list of ``txid`` s that can be used as inputs. + + Args: + owner (str): base58 encoded public key. + + Returns: + :obj:`list` of TransactionLink: list of ``txid`` s and ``output`` s + pointing to another transaction's condition + """ + return self.get_outputs_filtered(owner, include_spent=False) + + def get_outputs_filtered(self, owner, include_spent=True): + """ + Get a list of output links filtered on some criteria + """ + outputs = self.get_outputs(owner) + if not include_spent: + outputs = [o for o in outputs + if not self.get_spent(o.txid, o.output)] + return outputs def get_transactions_filtered(self, asset_id, operation=None): """ diff --git a/bigchaindb/web/routes.py b/bigchaindb/web/routes.py index 18133b3e..b20f8d40 100644 --- a/bigchaindb/web/routes.py +++ b/bigchaindb/web/routes.py @@ -5,7 +5,7 @@ from bigchaindb.web.views import ( info, statuses, transactions as tx, - unspents, + outputs, votes, ) @@ -30,7 +30,7 @@ ROUTES_API_V1 = [ r('statuses/', statuses.StatusApi), r('transactions/', tx.TransactionApi), r('transactions', tx.TransactionListApi), - r('unspents/', unspents.UnspentListApi), + r('outputs/', outputs.OutputListApi), r('votes/', votes.VotesApi), ] diff --git a/bigchaindb/web/views/outputs.py b/bigchaindb/web/views/outputs.py new file mode 100644 index 00000000..735a428f --- /dev/null +++ b/bigchaindb/web/views/outputs.py @@ -0,0 +1,28 @@ +from flask import current_app +from flask_restful import reqparse, Resource + +from bigchaindb.web.views import parameters + + +class OutputListApi(Resource): + def get(self): + """API endpoint to retrieve a list of links to transaction + outputs. + + Returns: + A :obj:`list` of :cls:`str` of links to outputs. + """ + parser = reqparse.RequestParser() + parser.add_argument('public_key', type=parameters.valid_ed25519, + required=True) + parser.add_argument('unspent', type=parameters.valid_bool) + args = parser.parse_args() + + pool = current_app.config['bigchain_pool'] + include_spent = not args['unspent'] + + with pool() as bigchain: + outputs = bigchain.get_outputs_filtered(args['public_key'], + include_spent) + # NOTE: We pass '..' as a path to create a valid relative URI + return [u.to_uri('..') for u in outputs] diff --git a/bigchaindb/web/views/unspents.py b/bigchaindb/web/views/unspents.py deleted file mode 100644 index 8cca995f..00000000 --- a/bigchaindb/web/views/unspents.py +++ /dev/null @@ -1,23 +0,0 @@ -from flask import current_app -from flask_restful import reqparse, Resource - - -class UnspentListApi(Resource): - def get(self): - """API endpoint to retrieve a list of links to transactions's - conditions that have not been used in any previous transaction. - - Returns: - A :obj:`list` of :cls:`str` of links to unfulfilled conditions. - """ - parser = reqparse.RequestParser() - parser.add_argument('public_key', type=str, location='args', - required=True) - args = parser.parse_args() - - pool = current_app.config['bigchain_pool'] - - with pool() as bigchain: - unspents = bigchain.get_owned_ids(args['public_key']) - # NOTE: We pass '..' as a path to create a valid relative URI - return [u.to_uri('..') for u in unspents] diff --git a/tests/common/test_transaction.py b/tests/common/test_transaction.py index da0fba17..7e70939d 100644 --- a/tests/common/test_transaction.py +++ b/tests/common/test_transaction.py @@ -436,6 +436,15 @@ def test_cast_transaction_link_to_boolean(): assert bool(TransactionLink(False, False)) is True +def test_transaction_link_eq(): + from bigchaindb.common.transaction import TransactionLink + + assert TransactionLink(1, 2) == TransactionLink(1, 2) + assert TransactionLink(2, 2) != TransactionLink(1, 2) + assert TransactionLink(1, 1) != TransactionLink(1, 2) + assert TransactionLink(2, 1) != TransactionLink(1, 2) + + def test_add_input_to_tx(user_input, asset_definition): from bigchaindb.common.transaction import Transaction diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index 1bf028b0..8a2040e8 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -1,6 +1,7 @@ from time import sleep import pytest +from unittest.mock import patch pytestmark = pytest.mark.bdb @@ -1156,3 +1157,38 @@ class TestMultipleInputs(object): # check that the other remain marked as unspent for unspent in transactions[1:]: assert b.get_spent(unspent.id, 0) is None + + +def test_get_owned_ids_calls_get_outputs_filtered(): + from bigchaindb.core import Bigchain + with patch('bigchaindb.core.Bigchain.get_outputs_filtered') as gof: + b = Bigchain() + res = b.get_owned_ids('abc') + gof.assert_called_once_with('abc', include_spent=False) + assert res == gof() + + +def test_get_outputs_filtered_only_unspent(): + from bigchaindb.common.transaction import TransactionLink + from bigchaindb.core import Bigchain + with patch('bigchaindb.core.Bigchain.get_outputs') as get_outputs: + get_outputs.return_value = [TransactionLink('a', 1), + TransactionLink('b', 2)] + with patch('bigchaindb.core.Bigchain.get_spent') as get_spent: + get_spent.side_effect = [True, False] + out = Bigchain().get_outputs_filtered('abc', include_spent=False) + get_outputs.assert_called_once_with('abc') + assert out == [TransactionLink('b', 2)] + + +def test_get_outputs_filtered(): + from bigchaindb.common.transaction import TransactionLink + from bigchaindb.core import Bigchain + with patch('bigchaindb.core.Bigchain.get_outputs') as get_outputs: + get_outputs.return_value = [TransactionLink('a', 1), + TransactionLink('b', 2)] + with patch('bigchaindb.core.Bigchain.get_spent') as get_spent: + out = Bigchain().get_outputs_filtered('abc') + get_outputs.assert_called_once_with('abc') + get_spent.assert_not_called() + assert out == get_outputs.return_value diff --git a/tests/web/test_outputs.py b/tests/web/test_outputs.py new file mode 100644 index 00000000..fd17d46d --- /dev/null +++ b/tests/web/test_outputs.py @@ -0,0 +1,49 @@ +import pytest +from unittest.mock import MagicMock, patch + +pytestmark = [pytest.mark.bdb, pytest.mark.usefixtures('inputs')] + +OUTPUTS_ENDPOINT = '/api/v1/outputs/' + + +def test_get_outputs_endpoint(client, user_pk): + m = MagicMock() + m.to_uri.side_effect = lambda s: 'a%sb' % s + with patch('bigchaindb.core.Bigchain.get_outputs_filtered') as gof: + gof.return_value = [m, m] + res = client.get(OUTPUTS_ENDPOINT + '?public_key={}'.format(user_pk)) + assert res.json == ['a..b', 'a..b'] + assert res.status_code == 200 + gof.assert_called_once_with(user_pk, True) + + +def test_get_outputs_endpoint_unspent(client, user_pk): + m = MagicMock() + m.to_uri.side_effect = lambda s: 'a%sb' % s + with patch('bigchaindb.core.Bigchain.get_outputs_filtered') as gof: + gof.return_value = [m] + params = '?unspent=true&public_key={}'.format(user_pk) + res = client.get(OUTPUTS_ENDPOINT + params) + assert res.json == ['a..b'] + assert res.status_code == 200 + gof.assert_called_once_with(user_pk, False) + + +def test_get_outputs_endpoint_without_public_key(client): + res = client.get(OUTPUTS_ENDPOINT) + assert res.status_code == 400 + + +def test_get_outputs_endpoint_with_invalid_public_key(client): + expected = {'message': {'public_key': 'Invalid base58 ed25519 key'}} + res = client.get(OUTPUTS_ENDPOINT + '?public_key=abc') + assert expected == res.json + assert res.status_code == 400 + + +def test_get_outputs_endpoint_with_invalid_unspent(client, user_pk): + expected = {'message': {'unspent': 'Boolean value must be "true" or "false" (lowercase)'}} + params = '?unspent=tru&public_key={}'.format(user_pk) + res = client.get(OUTPUTS_ENDPOINT + params) + assert expected == res.json + assert res.status_code == 400 diff --git a/tests/web/test_unspents.py b/tests/web/test_unspents.py deleted file mode 100644 index 9539c664..00000000 --- a/tests/web/test_unspents.py +++ /dev/null @@ -1,24 +0,0 @@ -import pytest - -pytestmark = [pytest.mark.bdb, pytest.mark.usefixtures('inputs')] - -UNSPENTS_ENDPOINT = '/api/v1/unspents/' - - -def test_get_unspents_endpoint(b, client, user_pk): - expected = [u.to_uri('..') for u in b.get_owned_ids(user_pk)] - res = client.get(UNSPENTS_ENDPOINT + '?public_key={}'.format(user_pk)) - assert expected == res.json - assert res.status_code == 200 - - -def test_get_unspents_endpoint_without_public_key(client): - res = client.get(UNSPENTS_ENDPOINT) - assert res.status_code == 400 - - -def test_get_unspents_endpoint_with_unused_public_key(client): - expected = [] - res = client.get(UNSPENTS_ENDPOINT + '?public_key=abc') - assert expected == res.json - assert res.status_code == 200