Skip to content

Commit 138cd12

Browse files
committed
[Bugfix][RandomImproveMultiAsset] Stop selecting more UTxOs when selected amount is already equal or larger than the requested.
1 parent e43737d commit 138cd12

File tree

2 files changed

+18
-12
lines changed

2 files changed

+18
-12
lines changed

pycardano/coinselection.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def select(self,
9797

9898
if change.coin < min_change_amount:
9999
additional, _ = self.select(available,
100-
[TransactionOutput(None, min_change_amount)],
100+
[TransactionOutput(None, min_change_amount - change.coin)],
101101
context,
102102
max_input_count - len(selected) if max_input_count else None,
103103
include_max_fee=False,
@@ -130,7 +130,7 @@ class RandomImproveMultiAsset(UTxOSelector):
130130
def __init__(self, random_generator: Optional[Iterable[int]] = None):
131131
self.random_generator = iter(random_generator) if random_generator else None
132132

133-
def _get_next_random(self, utxos: List[UTxO]) -> UTxO:
133+
def _get_next_random(self, utxos: List[UTxO]) -> Tuple[int, UTxO]:
134134
if not utxos:
135135
raise InputUTxODepletedException("Input UTxOs depleted!")
136136
if self.random_generator:
@@ -141,7 +141,7 @@ def _get_next_random(self, utxos: List[UTxO]) -> UTxO:
141141
raise UTxOSelectionException(f"Random index: {i} out of range!")
142142
else:
143143
i = random.randint(0, len(utxos) - 1)
144-
return utxos.pop(i)
144+
return i, utxos[i]
145145

146146
def _random_select_subset(self,
147147
amount: Value,
@@ -151,9 +151,10 @@ def _random_select_subset(self,
151151
while not amount <= selected_amount:
152152
if not remaining:
153153
raise InputUTxODepletedException("Input UTxOs depleted!")
154-
to_add = self._get_next_random(remaining)
154+
i, to_add = self._get_next_random(remaining)
155155
selected.append(to_add)
156156
selected_amount += to_add.output.amount
157+
remaining.pop(i)
157158

158159
@staticmethod
159160
def _split_by_asset(value: Value) -> List[Value]:
@@ -197,18 +198,21 @@ def _improve(self,
197198
ideal: Value,
198199
upper_bound: Value,
199200
max_input_count: int):
200-
if not remaining or self._find_diff_by_former(upper_bound, selected_amount) <= 0:
201+
if not remaining or self._find_diff_by_former(ideal, selected_amount) <= 0:
202+
# In case where there is no remaining UTxOs or we already selected more than ideal,
203+
# we cannot improve by randomly adding more UTxOs, therefore return immediate.
201204
return
202205
if max_input_count and len(selected) > max_input_count:
203206
raise MaxInputCountExceededException(f"Max input count: {max_input_count} exceeded!")
204207

205-
to_add = self._get_next_random(remaining)
208+
i, to_add = self._get_next_random(remaining)
206209
if abs(self._find_diff_by_former(ideal, selected_amount + to_add.output.amount)) < \
207210
abs(self._find_diff_by_former(ideal, selected_amount)) and \
208211
self._find_diff_by_former(upper_bound, selected_amount + to_add.output.amount) >= 0:
209212
selected.append(to_add)
210213
selected_amount += to_add.output.amount
211-
self._improve(selected, selected_amount, remaining, ideal, upper_bound, max_input_count)
214+
215+
self._improve(selected, selected_amount, remaining[:i] + remaining[i + 1:], ideal, upper_bound, max_input_count)
212216

213217
def select(self,
214218
utxos: List[UTxO],
@@ -254,7 +258,7 @@ def select(self,
254258

255259
if change.coin < min_change_amount:
256260
additional, _ = self.select(remaining,
257-
[TransactionOutput(None, min_change_amount)],
261+
[TransactionOutput(None, min_change_amount - change.coin)],
258262
context,
259263
max_input_count - len(selected) if max_input_count else None,
260264
include_max_fee=False,

test/pycardano/test_coinselection.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,13 @@ def test_no_fee_effect(self, chain_context):
163163

164164
def test_no_fee_but_respect_min_utxo(self, chain_context):
165165
request = [
166-
TransactionOutput.from_primitive([address, [10000000]])
166+
TransactionOutput.from_primitive([address, [500000]])
167167
]
168-
selected, change = self.selector.select(UTXOS, request, chain_context, include_max_fee=False,
169-
respect_min_utxo=True)
170-
assert selected == [UTXOS[9], UTXOS[8], UTXOS[0]] # UTXOS[0] is selected to respect min utxo amount
168+
# Only the first two UTxOs should be selected in this test case.
169+
# The first one is for the request amount, the second one is to respect min UTxO size.
170+
selected, change = RandomImproveMultiAsset(random_generator=[0, 0]).select(
171+
UTXOS, request, chain_context, include_max_fee=False, respect_min_utxo=True)
172+
assert selected == [UTXOS[0], UTXOS[1]] # UTXOS[1] is selected to respect min utxo amount
171173
assert_request_fulfilled(request, selected)
172174

173175
def test_utxo_depleted(self, chain_context):

0 commit comments

Comments
 (0)