From 4bb64fa0b83d337e44ba2b961ac9d8966c2fd8c6 Mon Sep 17 00:00:00 2001 From: Scott Sadler Date: Thu, 19 Jan 2017 13:02:58 +0100 Subject: [PATCH 1/4] generalise get_owned_ids to get_outputs and get_owned_ids --- bigchaindb/common/transaction.py | 2 +- bigchaindb/core.py | 27 ++++++++++++++++++++------- tests/common/test_transaction.py | 9 +++++++++ tests/db/test_bigchain_api.py | 13 +++++++++++++ 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index bda62663..563638ce 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..459c4908 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,10 +411,23 @@ 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 + 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 + """ + owned = [] + for tx_link in self.get_outputs(owner): + if not self.get_spent(tx_link.txid, tx_link.output): + owned.append(tx_link) return owned def get_transactions_filtered(self, asset_id, operation=None): diff --git a/tests/common/test_transaction.py b/tests/common/test_transaction.py index b4ef427c..56f81262 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..cfc2e93f 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,15 @@ 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(): + from bigchaindb.common.transaction import TransactionLink as TL + from bigchaindb.core import Bigchain + with patch('bigchaindb.core.Bigchain.get_outputs') as get_outputs: + get_outputs.return_value = [TL('a', 1), TL('b', 2)] + with patch('bigchaindb.core.Bigchain.get_spent') as get_spent: + get_spent.side_effect = [True, False] + out = Bigchain().get_owned_ids('abc') + assert get_outputs.called_once_with('abc') + assert out == [TL('b', 2)] From 897ffe81bca5dc5c4856e4f81a67d69e3a2ea091 Mon Sep 17 00:00:00 2001 From: Scott Sadler Date: Thu, 19 Jan 2017 15:49:30 +0100 Subject: [PATCH 2/4] outputs endpoint with unspent filter parameter --- bigchaindb/core.py | 16 +++++++---- bigchaindb/web/routes.py | 4 +-- bigchaindb/web/views/outputs.py | 28 ++++++++++++++++++ bigchaindb/web/views/unspents.py | 23 --------------- tests/db/test_bigchain_api.py | 27 ++++++++++++++++-- tests/web/test_outputs.py | 49 ++++++++++++++++++++++++++++++++ tests/web/test_unspents.py | 24 ---------------- 7 files changed, 114 insertions(+), 57 deletions(-) create mode 100644 bigchaindb/web/views/outputs.py delete mode 100644 bigchaindb/web/views/unspents.py create mode 100644 tests/web/test_outputs.py delete mode 100644 tests/web/test_unspents.py diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 459c4908..871c3707 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -424,11 +424,17 @@ class Bigchain(object): :obj:`list` of TransactionLink: list of ``txid`` s and ``output`` s pointing to another transaction's condition """ - owned = [] - for tx_link in self.get_outputs(owner): - if not self.get_spent(tx_link.txid, tx_link.output): - owned.append(tx_link) - return owned + 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/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index cfc2e93f..78c14a28 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -1159,13 +1159,34 @@ class TestMultipleInputs(object): assert b.get_spent(unspent.id, 0) is None -def test_get_owned_ids_calls(): +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 as TL from bigchaindb.core import Bigchain with patch('bigchaindb.core.Bigchain.get_outputs') as get_outputs: get_outputs.return_value = [TL('a', 1), TL('b', 2)] with patch('bigchaindb.core.Bigchain.get_spent') as get_spent: get_spent.side_effect = [True, False] - out = Bigchain().get_owned_ids('abc') - assert get_outputs.called_once_with('abc') + out = Bigchain().get_outputs_filtered('abc', include_spent=False) + get_outputs.assert_called_once_with('abc') assert out == [TL('b', 2)] + + +def test_get_outputs_filtered(): + from bigchaindb.common.transaction import TransactionLink as TL + from bigchaindb.core import Bigchain + with patch('bigchaindb.core.Bigchain.get_outputs') as get_outputs: + get_outputs.return_value = [TL('a', 1), TL('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..8fb418ea --- /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')] + +UNSPENTS_ENDPOINT = '/api/v1/outputs/' + + +def test_get_outputs_endpoint(client, user_pk): + m = MagicMock() + m.to_uri.side_effect = lambda s: s + with patch('bigchaindb.core.Bigchain.get_outputs_filtered') as gof: + gof.return_value = [m, m] + res = client.get(UNSPENTS_ENDPOINT + '?public_key={}'.format(user_pk)) + assert res.json == ["..", ".."] + 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: 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(UNSPENTS_ENDPOINT + params) + assert res.json == [".."] + 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(UNSPENTS_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(UNSPENTS_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(UNSPENTS_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 From af23ff5b65a008752a832f824d65dcf7baeb18e2 Mon Sep 17 00:00:00 2001 From: Scott Sadler Date: Tue, 24 Jan 2017 12:08:55 +0100 Subject: [PATCH 3/4] clean up use of double quotes, rename UNSPENTS_ENDPOINT, clarify test --- tests/db/test_bigchain_api.py | 4 ++-- tests/web/test_outputs.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index 78c14a28..bd0508de 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -1163,8 +1163,8 @@ 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) + res = b.get_owned_ids('abc') + gof.assert_called_once_with('abc', include_spent=False) assert res == gof() diff --git a/tests/web/test_outputs.py b/tests/web/test_outputs.py index 8fb418ea..fd17d46d 100644 --- a/tests/web/test_outputs.py +++ b/tests/web/test_outputs.py @@ -3,40 +3,40 @@ from unittest.mock import MagicMock, patch pytestmark = [pytest.mark.bdb, pytest.mark.usefixtures('inputs')] -UNSPENTS_ENDPOINT = '/api/v1/outputs/' +OUTPUTS_ENDPOINT = '/api/v1/outputs/' def test_get_outputs_endpoint(client, user_pk): m = MagicMock() - m.to_uri.side_effect = lambda s: s + 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(UNSPENTS_ENDPOINT + '?public_key={}'.format(user_pk)) - assert res.json == ["..", ".."] + 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: s + 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(UNSPENTS_ENDPOINT + params) - assert res.json == [".."] + 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(UNSPENTS_ENDPOINT) + 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(UNSPENTS_ENDPOINT + '?public_key=abc') + res = client.get(OUTPUTS_ENDPOINT + '?public_key=abc') assert expected == res.json assert res.status_code == 400 @@ -44,6 +44,6 @@ def test_get_outputs_endpoint_with_invalid_public_key(client): 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(UNSPENTS_ENDPOINT + params) + res = client.get(OUTPUTS_ENDPOINT + params) assert expected == res.json assert res.status_code == 400 From e3317b370bbeecb9177ce142703dccdd9717b95b Mon Sep 17 00:00:00 2001 From: Scott Sadler Date: Tue, 24 Jan 2017 12:11:21 +0100 Subject: [PATCH 4/4] don't rename TransactionLink to TL --- tests/db/test_bigchain_api.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index bd0508de..8a2040e8 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -1169,22 +1169,24 @@ def test_get_owned_ids_calls_get_outputs_filtered(): def test_get_outputs_filtered_only_unspent(): - from bigchaindb.common.transaction import TransactionLink as TL + 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 = [TL('a', 1), TL('b', 2)] + 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 == [TL('b', 2)] + assert out == [TransactionLink('b', 2)] def test_get_outputs_filtered(): - from bigchaindb.common.transaction import TransactionLink as TL + 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 = [TL('a', 1), TL('b', 2)] + 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')