Rename unspent -> spent in outputs endpoint

- If spent=None return all outputs
- If spent=True return only spent outputs
- If spent=False return only unspent outputs
- Updated documentation
- Add the ability to return only spent outputs in core
- Added and update tests
This commit is contained in:
Rodolphe Marques 2017-06-15 14:08:38 +02:00
parent 8d60796765
commit b523ba3fe5
8 changed files with 180 additions and 26 deletions

View File

@ -402,20 +402,33 @@ class Bigchain(object):
: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)
return self.get_outputs_filtered(owner, spent=False)
@property
def fastquery(self):
return fastquery.FastQuery(self.connection, self.me)
def get_outputs_filtered(self, owner, include_spent=True):
def get_outputs_filtered(self, owner, spent=None):
"""
Get a list of output links filtered on some criteria
Args:
owner (str): base58 encoded public_key.
spent (bool): If ``True`` return only the spent outputs. If
``False`` return only unspent outputs. If spent is
not specified (``None``) return all outputs.
Returns:
:obj:`list` of TransactionLink: list of ``txid`` s and ``output`` s
pointing to another transaction's condition
"""
outputs = self.fastquery.get_outputs_by_public_key(owner)
if not include_spent:
outputs = self.fastquery.filter_spent_outputs(outputs)
return outputs
if spent is None:
return outputs
elif spent is True:
return self.fastquery.filter_unspent_outputs(outputs)
elif spent is False:
return self.fastquery.filter_spent_outputs(outputs)
def get_transactions_filtered(self, asset_id, operation=None):
"""

View File

@ -68,3 +68,18 @@ class FastQuery:
for tx in txs
for input_ in tx['inputs']}
return [ff for ff in outputs if ff not in spends]
def filter_unspent_outputs(self, outputs):
"""
Remove outputs that have not been spent
Args:
outputs: list of TransactionLink
"""
links = [o.to_dict() for o in outputs]
res = query.get_spending_transactions(self.connection, links)
txs = [tx for _, tx in self.filter_valid_items(res)]
spends = {TransactionLink.from_dict(input_['fulfills'])
for tx in txs
for input_ in tx['inputs']}
return [ff for ff in outputs if ff in spends]

View File

@ -15,14 +15,12 @@ class OutputListApi(Resource):
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()
parser.add_argument('spent', type=parameters.valid_bool)
args = parser.parse_args(strict=True)
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)
args['spent'])
return [{'transaction_id': output.txid, 'output': output.output}
for output in outputs]

View File

@ -166,21 +166,29 @@ Transaction Outputs
-------------------
The ``/api/v1/outputs`` endpoint returns transactions outputs filtered by a
given public key, and optionally filtered to only include outputs that have
not already been spent.
given public key, and optionally filtered to only include either spent or
unspent outputs.
.. http:get:: /api/v1/outputs?public_key={public_key}
.. http:get:: /api/v1/outputs
Get transaction outputs by public key. The `public_key` parameter must be
Get transaction outputs by public key. The ``public_key`` parameter must be
a base58 encoded ed25519 public key associated with transaction output
ownership.
Returns a list of transaction outputs.
:param public_key: Base58 encoded public key associated with output ownership. This parameter is mandatory and without it the endpoint will return a ``400`` response code.
:param unspent: Boolean value ("true" or "false") indicating if the result set should be limited to outputs that are available to spend. Defaults to "false".
:param public_key: Base58 encoded public key associated with output
ownership. This parameter is mandatory and without it
the endpoint will return a ``400`` response code.
:param spent: Boolean value ("true" or "false") indicating if the result set
should include only spent or only unspent outputs. If not
specified the result includes all the outputs (both spent
and unspent).
.. http:get:: /api/v1/outputs?public_key={public_key}
Return all outputs, both spent and unspent, for the ``public_key``.
**Example request**:
@ -210,6 +218,62 @@ not already been spent.
:statuscode 200: A list of outputs were found and returned in the body of the response.
:statuscode 400: The request wasn't understood by the server, e.g. the ``public_key`` querystring was not included in the request.
.. http:get:: /api/v1/outputs?public_key={public_key}&spent=true
Return all **spent** outputs for ``public_key``.
**Example request**:
.. sourcecode:: http
GET /api/v1/outputs?public_key=1AAAbbb...ccc&spent=true HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"output": 0,
"transaction_id": "2d431073e1477f3073a4693ac7ff9be5634751de1b8abaa1f4e19548ef0b4b0e"
}
]
:statuscode 200: A list of outputs were found and returned in the body of the response.
:statuscode 400: The request wasn't understood by the server, e.g. the ``public_key`` querystring was not included in the request.
.. http:get:: /api/v1/outputs?public_key={public_key}&spent=false
Return all **unspent** outputs for ``public_key``.
**Example request**:
.. sourcecode:: http
GET /api/v1/outputs?public_key=1AAAbbb...ccc&spent=false HTTP/1.1
Host: example.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"output": 1,
"transaction_id": "2d431073e1477f3073a4693ac7ff9be5634751de1b8abaa1f4e19548ef0b4b0e"
}
]
:statuscode 200: A list of outputs were found and returned in the body of the response.
:statuscode 400: The request wasn't understood by the server, e.g. the ``public_key`` querystring was not included in the request.
Statuses
--------------------------------

View File

@ -1194,7 +1194,7 @@ def test_get_owned_ids_calls_get_outputs_filtered():
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)
gof.assert_called_once_with('abc', spent=False)
assert res == gof()
@ -1206,21 +1206,36 @@ def test_get_outputs_filtered_only_unspent():
TransactionLink('b', 2)]
with patch('bigchaindb.fastquery.FastQuery.filter_spent_outputs') as filter_spent:
filter_spent.return_value = [TransactionLink('b', 2)]
out = Bigchain().get_outputs_filtered('abc', include_spent=False)
out = Bigchain().get_outputs_filtered('abc', spent=False)
get_outputs.assert_called_once_with('abc')
assert out == [TransactionLink('b', 2)]
def test_get_outputs_filtered():
def test_get_outputs_filtered_only_spent():
from bigchaindb.common.transaction import TransactionLink
from bigchaindb.core import Bigchain
with patch('bigchaindb.fastquery.FastQuery.get_outputs_by_public_key') as get_outputs:
get_outputs.return_value = [TransactionLink('a', 1),
TransactionLink('b', 2)]
with patch('bigchaindb.fastquery.FastQuery.filter_spent_outputs') as filter_spent:
out = Bigchain().get_outputs_filtered('abc')
with patch('bigchaindb.fastquery.FastQuery.filter_unspent_outputs') as filter_spent:
filter_spent.return_value = [TransactionLink('b', 2)]
out = Bigchain().get_outputs_filtered('abc', spent=True)
get_outputs.assert_called_once_with('abc')
assert out == [TransactionLink('b', 2)]
@patch('bigchaindb.fastquery.FastQuery.filter_unspent_outputs')
@patch('bigchaindb.fastquery.FastQuery.filter_spent_outputs')
def test_get_outputs_filtered(filter_spent, filter_unspent):
from bigchaindb.common.transaction import TransactionLink
from bigchaindb.core import Bigchain
with patch('bigchaindb.fastquery.FastQuery.get_outputs_by_public_key') as get_outputs:
get_outputs.return_value = [TransactionLink('a', 1),
TransactionLink('b', 2)]
out = Bigchain().get_outputs_filtered('abc')
get_outputs.assert_called_once_with('abc')
filter_spent.assert_not_called()
filter_unspent.assert_not_called()
assert out == get_outputs.return_value

View File

@ -127,4 +127,4 @@ def test_get_spent_issue_1271(b, alice, bob, carol):
assert b.get_spent(tx_2.id, 0) == tx_5
assert not b.get_spent(tx_5.id, 0)
assert b.get_outputs_filtered(alice.public_key)
assert b.get_outputs_filtered(alice.public_key, include_spent=False)
assert b.get_outputs_filtered(alice.public_key, spent=False)

View File

@ -84,3 +84,39 @@ def test_filter_spent_outputs(b, user_pk):
tx2.to_inputs()[0].fulfills,
tx4.to_inputs()[0].fulfills
}
def test_filter_unspent_outputs(b, user_pk):
out = [([user_pk], 1)]
tx1 = Transaction.create([user_pk], out * 3)
# There are 3 inputs
inputs = tx1.to_inputs()
# Each spent individually
tx2 = Transaction.transfer([inputs[0]], out, tx1.id)
tx3 = Transaction.transfer([inputs[1]], out, tx1.id)
tx4 = Transaction.transfer([inputs[2]], out, tx1.id)
# The CREATE and first TRANSFER are valid. tx2 produces a new unspent.
for tx in [tx1, tx2]:
block = Block([tx])
b.write_block(block)
b.write_vote(b.vote(block.id, '', True))
# The second TRANSFER is invalid. inputs[1] remains unspent.
block = Block([tx3])
b.write_block(block)
b.write_vote(b.vote(block.id, '', False))
# The third TRANSFER is undecided. It procuces a new unspent.
block = Block([tx4])
b.write_block(block)
outputs = b.fastquery.get_outputs_by_public_key(user_pk)
spents = b.fastquery.filter_unspent_outputs(outputs)
assert set(spents) == {
inputs[0].fulfills,
inputs[2].fulfills
}

View File

@ -18,7 +18,7 @@ def test_get_outputs_endpoint(client, user_pk):
{'transaction_id': 'a', 'output': 0}
]
assert res.status_code == 200
gof.assert_called_once_with(user_pk, True)
gof.assert_called_once_with(user_pk, None)
def test_get_outputs_endpoint_unspent(client, user_pk):
@ -27,13 +27,26 @@ def test_get_outputs_endpoint_unspent(client, user_pk):
m.output = 0
with patch('bigchaindb.core.Bigchain.get_outputs_filtered') as gof:
gof.return_value = [m]
params = '?unspent=true&public_key={}'.format(user_pk)
params = '?spent=False&public_key={}'.format(user_pk)
res = client.get(OUTPUTS_ENDPOINT + params)
assert res.json == [{'transaction_id': 'a', 'output': 0}]
assert res.status_code == 200
gof.assert_called_once_with(user_pk, False)
def test_get_outputs_endpoint_spent(client, user_pk):
m = MagicMock()
m.txid = 'a'
m.output = 0
with patch('bigchaindb.core.Bigchain.get_outputs_filtered') as gof:
gof.return_value = [m]
params = '?spent=true&public_key={}'.format(user_pk)
res = client.get(OUTPUTS_ENDPOINT + params)
assert res.json == [{'transaction_id': 'a', 'output': 0}]
assert res.status_code == 200
gof.assert_called_once_with(user_pk, True)
def test_get_outputs_endpoint_without_public_key(client):
res = client.get(OUTPUTS_ENDPOINT)
assert res.status_code == 400
@ -47,8 +60,8 @@ 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)
expected = {'message': {'spent': 'Boolean value must be "true" or "false" (lowercase)'}}
params = '?spent=tru&public_key={}'.format(user_pk)
res = client.get(OUTPUTS_ENDPOINT + params)
assert expected == res.json
assert res.status_code == 400