Skip to content
This repository was archived by the owner on Jan 13, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions iota/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,33 @@ def prepare_transfer(self, transfers, inputs=None, change_address=None):
changeAddress = change_address,
)

def promote_transaction(
self,
transaction,
depth,
min_weight_magnitude = None,
):
# type: (TransactionHash, int, Optional[int]) -> dict
"""
Promotes a transaction by adding spam on top of it.

:return:
Dict containing the following values::

{
'bundle': Bundle,
The newly-published bundle.
}
"""
if min_weight_magnitude is None:
min_weight_magnitude = self.default_min_weight_magnitude

return extended.PromoteTransactionCommand(self.adapter)(
transaction = transaction,
depth = depth,
minWeightMagnitude = min_weight_magnitude,
)

def replay_bundle(
self,
transaction,
Expand Down
18 changes: 18 additions & 0 deletions iota/commands/core/get_transactions_to_approve.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,26 @@ class GetTransactionsToApproveRequestFilter(RequestFilter):
def __init__(self):
super(GetTransactionsToApproveRequestFilter, self).__init__({
'depth': f.Required | f.Type(int) | f.Min(1),

'reference': Trytes(result_type=TransactionHash),
},

allow_missing_keys = {
'reference',
})

def _apply(self, value):
value = super(GetTransactionsToApproveRequestFilter, self)._apply(value) # type: dict

if self._has_errors:
return value

# Remove reference if null.
if value['reference'] is None:
del value['reference']

return value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ Could we add a couple of unit tests for GetTransactionsToApproveRequestFilter, related to reference?

  • Not TrytesCompatible.
  • Included in resulting dict if not null.



class GetTransactionsToApproveResponseFilter(ResponseFilter):
def __init__(self):
Expand Down
1 change: 1 addition & 0 deletions iota/commands/extended/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .get_new_addresses import *
from .get_transfers import *
from .prepare_transfer import *
from .promote_transaction import *
from .replay_bundle import *
from .send_transfer import *
from .send_trytes import *
68 changes: 68 additions & 0 deletions iota/commands/extended/promote_transaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# coding=utf-8
from __future__ import absolute_import, division, print_function, \
unicode_literals

import filters as f
from iota import (
Bundle, TransactionHash, Address, ProposedTransaction, BadApiResponse,
)
from iota.commands import FilterCommand, RequestFilter
from iota.commands.core.check_consistency import CheckConsistencyCommand
from iota.commands.extended.send_transfer import SendTransferCommand
from iota.filters import Trytes

__all__ = [
'PromoteTransactionCommand',
]


class PromoteTransactionCommand(FilterCommand):
"""
Executes ``promoteTransaction`` extended API command.
See :py:meth:`iota.api.Iota.promote_transaction` for more information.
"""
command = 'promoteTransaction'

def get_request_filter(self):
return PromoteTransactionRequestFilter()

def get_response_filter(self):
pass

def _execute(self, request):
depth = request['depth'] # type: int
min_weight_magnitude = request['minWeightMagnitude'] # type: int
transaction = request['transaction'] # type: TransactionHash

cc_response = CheckConsistencyCommand(self.adapter)(tails=[transaction])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to use the new is_promotable helper here, but we don't have a reference to the Iota class. I don't think this is a problem as it doesn't cause much duplication, but if we have more helper functions that we want to use from these commands in the future, it might need some thought on how to achieve that.

Copy link
Contributor

@todofixthis todofixthis Jan 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm... interesting.

What do you think about this?

class Helpers:
  def __init__(self, adapter):
     ...

Then PromoteTransactionCommand can do Helpers(self.adapter).is_promotable(...) (and minor adjustment to how Iota.helpers is initialised).

The tradeoff is that helpers won't have access to an Iota instance, but that hasn't proved to be an obstacle for command classes so far (:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that mean that the current is_promotable should be changed from:

  def is_promotable(self, tail):
    return self.api.check_consistency(tails=[tail])['state']

To:

  from iota.commands.core.check_consistency import CheckConsistencyCommand

  def is_promotable(self, tail):
    return CheckConsistencyCommand(self.adapter)(tails=[tail])['state']

I don't see a problem with that.

Copy link
Contributor

@todofixthis todofixthis Jan 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aye, I think that would... oh. Hm, we might end up with circular references that way.

Eh, I guess we could import the commands locally in the helper methods. The alternative, I think, would be to create helper classes, similar to what we're doing for commands. That feels a bit cathedral-ish to me, though; I dunno.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I can foresee circular dependencies. I'm okay with keeping it as is (not using the helper call) for the moment until a later date.

if cc_response['state'] is False:
raise BadApiResponse(
'Transaction {transaction} is not promotable. '
'You should reattach first.'.format(transaction=transaction)
)

spam_transfer = ProposedTransaction(
address=Address(b''),
value=0,
)

return SendTransferCommand(self.adapter)(
seed=spam_transfer.address,
depth=depth,
transfers=[spam_transfer],
minWeightMagnitude=min_weight_magnitude,
reference=transaction,
)


class PromoteTransactionRequestFilter(RequestFilter):
def __init__(self):
super(PromoteTransactionRequestFilter, self).__init__({
'depth': f.Required | f.Type(int) | f.Min(1),
'transaction': f.Required | Trytes(result_type=TransactionHash),

# Loosely-validated; testnet nodes require a different value than
# mainnet.
'minWeightMagnitude': f.Required | f.Type(int) | f.Min(1),
})
7 changes: 6 additions & 1 deletion iota/commands/extended/send_transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import List, Optional

import filters as f
from iota import Address, Bundle, ProposedTransaction
from iota import Address, Bundle, ProposedTransaction, TransactionHash
from iota.commands import FilterCommand, RequestFilter
from iota.commands.extended.prepare_transfer import PrepareTransferCommand
from iota.commands.extended.send_trytes import SendTrytesCommand
Expand Down Expand Up @@ -38,6 +38,7 @@ def _execute(self, request):
min_weight_magnitude = request['minWeightMagnitude'] # type: int
seed = request['seed'] # type: Seed
transfers = request['transfers'] # type: List[ProposedTransaction]
reference = request['reference'] # type: Optional[TransactionHash]

pt_response = PrepareTransferCommand(self.adapter)(
changeAddress = change_address,
Expand All @@ -50,6 +51,7 @@ def _execute(self, request):
depth = depth,
minWeightMagnitude = min_weight_magnitude,
trytes = pt_response['trytes'],
reference = reference,
)

return {
Expand Down Expand Up @@ -82,10 +84,13 @@ def __init__(self):
# Note that ``inputs`` is allowed to be an empty array.
'inputs':
f.Array | f.FilterRepeater(f.Required | Trytes(result_type=Address)),

'reference': Trytes(result_type=TransactionHash),
},

allow_missing_keys = {
'changeAddress',
'inputs',
'reference',
},
)
14 changes: 12 additions & 2 deletions iota/commands/extended/send_trytes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import List

import filters as f
from iota import TransactionTrytes, TryteString
from iota import TransactionTrytes, TryteString, TransactionHash
from iota.commands import FilterCommand, RequestFilter
from iota.commands.core.attach_to_tangle import AttachToTangleCommand
from iota.commands.core.get_transactions_to_approve import \
Expand Down Expand Up @@ -36,10 +36,14 @@ def _execute(self, request):
depth = request['depth'] # type: int
min_weight_magnitude = request['minWeightMagnitude'] # type: int
trytes = request['trytes'] # type: List[TryteString]
reference = request['reference'] # type: Optional[TransactionHash]

# Call ``getTransactionsToApprove`` to locate trunk and branch
# transactions so that we can attach the bundle to the Tangle.
gta_response = GetTransactionsToApproveCommand(self.adapter)(depth=depth)
gta_response = GetTransactionsToApproveCommand(self.adapter)(
depth=depth,
reference=reference,
)

att_response = AttachToTangleCommand(self.adapter)(
branchTransaction = gta_response.get('branchTransaction'),
Expand Down Expand Up @@ -72,4 +76,10 @@ def __init__(self):
# Loosely-validated; testnet nodes require a different value than
# mainnet.
'minWeightMagnitude': f.Required | f.Type(int) | f.Min(1),

'reference': Trytes(result_type=TransactionHash),
},

allow_missing_keys = {
'reference',
})
60 changes: 58 additions & 2 deletions test/commands/core/get_transactions_to_approve_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,43 @@
from iota.adapter import MockAdapter
from iota.commands.core.get_transactions_to_approve import \
GetTransactionsToApproveCommand
from iota.filters import Trytes


class GetTransactionsToApproveRequestFilterTestCase(BaseFilterTestCase):
filter_type =\
GetTransactionsToApproveCommand(MockAdapter()).get_request_filter
skip_value_check = True

def test_pass_happy_path(self):
def setUp(self):
super(GetTransactionsToApproveRequestFilterTestCase, self).setUp()

# Define some tryte sequences that we can reuse between tests.
self.trytes1 = (
b'TESTVALUEONE9DONTUSEINPRODUCTION99999JBW'
b'GEC99GBXFFBCHAEJHLC9DX9EEPAI9ICVCKBX9FFII'
)

def test_pass_happy_path_without_reference(self):
"""
Request is valid without reference.
"""
request = {
'depth': 100,
}

filter_ = self._filter(request)

self.assertFilterPasses(filter_)
self.assertDictEqual(filter_.cleaned_data, request)

def test_pass_happy_path_with_reference(self):
"""
Request is valid.
Request is valid with reference.
"""
request = {
'depth': 100,
'reference': TransactionHash(self.trytes1),
}

filter_ = self._filter(request)
Expand Down Expand Up @@ -115,6 +139,38 @@ def test_fail_depth_too_small(self):
},
)

def test_fail_reference_wrong_type(self):
"""
``reference`` is not a TrytesCompatible value.
"""
self.assertFilterErrors(
{
'reference': 42,

'depth': 100,
},

{
'reference': [f.Type.CODE_WRONG_TYPE],
},
)

def test_fail_reference_not_trytes(self):
"""
``reference`` contains invalid characters.
"""
self.assertFilterErrors(
{
'reference': b'not valid; must contain only uppercase and "9"',

'depth': 100,
},

{
'reference': [Trytes.CODE_NOT_TRYTES],
},
)


class GetTransactionsToApproveResponseFilterTestCase(BaseFilterTestCase):
filter_type =\
Expand Down
Loading