Skip to content
This repository has been archived by the owner on Jan 13, 2023. It is now read-only.

Commit

Permalink
Merge pull request #127 from scottbelden/promote_transaction
Browse files Browse the repository at this point in the history
add promote_transaction_api
  • Loading branch information
todofixthis committed Jan 11, 2018
2 parents 31fa921 + ab729af commit 27f74d6
Show file tree
Hide file tree
Showing 10 changed files with 610 additions and 7 deletions.
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


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])
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

0 comments on commit 27f74d6

Please sign in to comment.