Skip to content

Commit 7183e6b

Browse files
feat: add a warning for high tx fee
1 parent a3c8f39 commit 7183e6b

File tree

5 files changed

+128
-4
lines changed

5 files changed

+128
-4
lines changed

l10n/messages.pot

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
#, fuzzy
77
msgid ""
88
msgstr ""
9-
"Project-Id-Version: seedsigner 0.8.5-rc1\n"
9+
"Project-Id-Version: seedsigner 0.8.5\n"
1010
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
11-
"POT-Creation-Date: 2025-01-28 13:28-0600\n"
11+
"POT-Creation-Date: 2025-04-13 21:25+0000\n"
1212
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
1313
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
1414
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -854,6 +854,20 @@ msgid ""
854854
" wallet."
855855
msgstr ""
856856

857+
#: src/seedsigner/views/psbt_views.py
858+
msgid "High Fee!"
859+
msgstr ""
860+
861+
#. Variable is the percentage of the total output value (excluding change) that
862+
#. the fee exceeds. (e.g. "This PSBT has a fee higher than 25% of the total
863+
#. output value (excluding change).")
864+
#: src/seedsigner/views/psbt_views.py
865+
#, python-format
866+
msgid ""
867+
"This PSBT has a fee higher than {}% of the total output value (excluding "
868+
"change)."
869+
msgstr ""
870+
857871
#. Future-tense used to indicate that this transaction will send this amount,
858872
#. as opposed to "Send" on its own which could be misread as an instant command
859873
#. (e.g. "Send Now").
@@ -1254,11 +1268,11 @@ msgid "Review SeedQR"
12541268
msgstr ""
12551269

12561270
#: src/seedsigner/views/seed_views.py
1257-
msgid "Your transcribed SeedQR successfully scanned and yielded the same seed."
1271+
msgid "Your transcribed SeedQR could not be read!"
12581272
msgstr ""
12591273

12601274
#: src/seedsigner/views/seed_views.py
1261-
msgid "Your transcribed SeedQR could not be read!"
1275+
msgid "Your transcribed SeedQR successfully scanned and yielded the same seed."
12621276
msgstr ""
12631277

12641278
#: src/seedsigner/views/seed_views.py

src/seedsigner/models/psbt_parser.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,3 +390,13 @@ def verify_multisig_output(self, descriptor: Descriptor, change_num: int) -> boo
390390
is_owner = descriptor.owns(output)
391391
# print(f"{self.psbt.tx.vout[i].script_pubkey.address()} | {output.value} | {is_owner}")
392392
return is_owner
393+
394+
395+
def get_sum_of_outputs_excluding_change(self):
396+
"""
397+
Returns the sum of all outputs excluding change amount.
398+
This is used to to calculate whether the fee is relatively high
399+
and show a warning screen if it is.
400+
"""
401+
total_outputs = sum(out.value for out in self.psbt.tx.vout)
402+
return total_outputs - self.change_amount

src/seedsigner/views/psbt_views.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ def run(self):
8383

8484

8585
class PSBTOverviewView(View):
86+
# TODO: Possibly make this configurable via settings
87+
HIGH_FEES_WARNING_THRESHOLD = 25 # in percent
88+
8689
def __init__(self):
8790
super().__init__()
8891

@@ -151,10 +154,17 @@ def run(self):
151154
self.controller.psbt_seed = None
152155
return Destination(BackStackView)
153156

157+
sum_of_all_outputs_excluding_change = psbt_parser.get_sum_of_outputs_excluding_change()
158+
154159
# expecting p2sh (legacy multisig) and p2pkh to have no policy set
155160
# skip change warning and psbt math view
156161
if psbt_parser.policy == None:
157162
return Destination(PSBTUnsupportedScriptTypeWarningView)
163+
164+
# High Fee warning if fee amount > <HIGH_FEES_WARNING_THRESHOLD> % of sum of all outputs excluding change
165+
# Skip the warning when there is no output other than change
166+
elif sum_of_all_outputs_excluding_change > 0 and psbt_parser.fee_amount > self.HIGH_FEES_WARNING_THRESHOLD/100 * (sum_of_all_outputs_excluding_change):
167+
return Destination(PSBTHighFeeWarningView, view_args={"warning_threshold_percent": self.HIGH_FEES_WARNING_THRESHOLD})
158168

159169
elif psbt_parser.change_amount == 0:
160170
return Destination(PSBTNoChangeWarningView)
@@ -204,6 +214,38 @@ def run(self):
204214

205215

206216

217+
class PSBTHighFeeWarningView(View):
218+
def __init__(self, warning_threshold_percent: int):
219+
super().__init__()
220+
221+
self.warning_threshold_percent = warning_threshold_percent
222+
223+
def run(self):
224+
selected_menu_num = WarningScreen(
225+
status_headline=_("High Fee!"),
226+
# TRANSLATOR_NOTE: Variable is the percentage of the total output value (excluding change) that the fee exceeds. (e.g. "This PSBT has a fee higher than 25% of the total output value (excluding change).")
227+
text=_("This PSBT has a fee higher than {}% of the total output value (excluding change).").format(self.warning_threshold_percent),
228+
button_data=[ButtonOption("Continue")],
229+
).display()
230+
231+
if selected_menu_num == RET_CODE__BACK_BUTTON:
232+
return Destination(BackStackView)
233+
234+
# PSBT may have high fee + no change
235+
if self.controller.psbt_parser.change_amount == 0:
236+
return Destination(
237+
PSBTNoChangeWarningView,
238+
skip_current_view=True, # Prevent going BACK to WarningViews
239+
)
240+
241+
else:
242+
return Destination(
243+
PSBTMathView,
244+
skip_current_view=True, # Prevent going BACK to WarningViews
245+
)
246+
247+
248+
207249
class PSBTMathView(View):
208250
"""
209251
Follows the Overview pictogram. Shows:

tests/screenshot_generator/generator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ def PSBTOpReturnView_raw_hex_data_cb_before():
324324
ScreenshotConfig(psbt_views.PSBTOverviewView, run_before=load_basic_psbt_cb),
325325
ScreenshotConfig(psbt_views.PSBTUnsupportedScriptTypeWarningView),
326326
ScreenshotConfig(psbt_views.PSBTNoChangeWarningView),
327+
ScreenshotConfig(psbt_views.PSBTHighFeeWarningView, dict(warning_threshold_percent=25)),
327328
ScreenshotConfig(psbt_views.PSBTMathView),
328329
ScreenshotConfig(psbt_views.PSBTAddressDetailsView, dict(address_num=0)),
329330

tests/test_psbt_parser.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from binascii import a2b_base64
2+
from types import SimpleNamespace
23
from embit import psbt
34
from embit.descriptor import Descriptor
5+
import pytest
46

57
from seedsigner.models.psbt_parser import PSBTParser
68
from seedsigner.models.seed import Seed
@@ -201,3 +203,58 @@ def test_parse_op_return_content():
201203
assert psbt_parser.change_amount == 99992296
202204
assert psbt_parser.destination_addresses == []
203205
assert psbt_parser.destination_amounts == []
206+
207+
208+
@pytest.mark.parametrize("vout_values, change_amount, expected", [
209+
# single destination + single change
210+
([100, 200], 200, 100),
211+
# multiple destinations + single change
212+
([50, 75, 25], 25, 50 + 75),
213+
# no change outputs at all
214+
([10, 20, 30], 0, 10 + 20 + 30),
215+
# only change outputs
216+
([123], 123, 0),
217+
])
218+
def test_get_sum_of_outputs_excluding_change_logic(vout_values, change_amount, expected):
219+
"""
220+
get_sum_of_outputs_excluding_change() should return
221+
sum(vout_values) - change_amount.
222+
"""
223+
# Build a dummy parser without running .parse()
224+
parser = PSBTParser.__new__(PSBTParser)
225+
226+
# Stub out parser.psbt.tx.vout as list of objects with a .value attribute
227+
parser.psbt = SimpleNamespace(
228+
tx=SimpleNamespace(
229+
vout=[SimpleNamespace(value=v) for v in vout_values]
230+
)
231+
)
232+
parser.change_amount = change_amount
233+
234+
assert parser.get_sum_of_outputs_excluding_change() == expected
235+
236+
237+
def test_get_sum_of_outputs_excluding_change_integration():
238+
"""
239+
Integration test for get_sum_of_outputs_excluding_change() using a real PSBT.
240+
PSBT from test_p2tr_change_detection has:
241+
- Destination output: 319,049,328 sats
242+
- Change output: 2,871,443,918 sats
243+
- Total outputs: 3,190,493,246 sats
244+
- Excluding change: 319,049,328 sats
245+
"""
246+
# This is the same PSBT as in test_p2tr_change_detection
247+
psbt_base64 = "cHNidP8BAIkCAAAAAf8upuiIWF1VTgC/Q8ZWRrameRigaXpRcQcBe8ye+TK3AQAAAAAXCgAAAs7BJqsAAAAAIlEgGKqNQ7yF4+yFrrscHnjrbEHiJFExhR903ze43FtOH3BwTgQTAAAAACJRINBe93RcrOYO4UVLLE0y8pzvblOKQWcoQ0obCey8nA5GAAAAAE8BBDWHzwNMUx9OgAAAAJdr+WtwWfVa6IPbpKZ4KgRC0clbm11Gl155IPA27n2FAvQCrFGH6Ac2U0Gcy1IH5f5ltgUBDz2+fe8iqL6JzZdgEDlK7RRWAACAAQAAgAAAAIAAAQB9AgAAAAGAKOOUFIzw9pbRDaZ7F0DYhLImrdMn//OSm++ff5VNdAAAAAAAAQAAAAKsjLwAAAAAABYAFKEcuxvXmB3rWHSqSviP5mrKMZoL2RArvgAAAAAiUSBGU0Lg5fx/ECsB1Z4ZUqXQFSLFnlmpm0rm5R2l599h2AAAAAABASvZECu+AAAAACJRIEZTQuDl/H8QKwHVnhlSpdAVIsWeWambSublHaXn32HYAQMEAAAAACEWF7hZVn7pIDR429kAn/WDeQiWjZey1iGHztsL1H83QLMZADlK7RRWAACAAQAAgAAAAIABAAAAAAAAAAEXIBe4WVZ+6SA0eNvZAJ/1g3kIlo2XstYhh87bC9R/N0CzACEHbJdqWyMxF2eOPr6YRXUJmry04HUbgKyeM2IZeG+NI9AZADlK7RRWAACAAQAAgAAAAIABAAAAAQAAAAEFIGyXalsjMRdnjj6+mEV1CZq8tOB1G4CsnjNiGXhvjSPQAAA="
248+
249+
raw = a2b_base64(psbt_base64)
250+
tx = psbt.PSBT.parse(raw)
251+
252+
mnemonic = "goddess rough corn exclude cream trial fee trumpet million prevent gaze power".split()
253+
pw = ""
254+
seed = Seed(mnemonic, passphrase=pw)
255+
256+
pp = PSBTParser(p=tx, seed=seed, network=SettingsConstants.REGTEST)
257+
258+
# Expected: only the destination output (319,049,328 sats)
259+
expected = 319049328
260+
assert pp.get_sum_of_outputs_excluding_change() == expected

0 commit comments

Comments
 (0)