From 1cab2f9d6d45c3f8b9d15f28e954c355834c4123 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Sat, 15 Mar 2025 18:24:53 +0000 Subject: [PATCH] tx.new: support relative fees < 1 unit; add test --- mmgen/proto/btc/tx/new.py | 4 ++-- mmgen/proto/eth/tx/base.py | 5 ++--- mmgen/proto/eth/tx/new.py | 4 ++-- mmgen/tx/new.py | 15 +++++++++------ test/modtest_d/ut_amt.py | 27 ++++++++++++++++++++++++++- 5 files changed, 41 insertions(+), 14 deletions(-) diff --git a/mmgen/proto/btc/tx/new.py b/mmgen/proto/btc/tx/new.py index 7431010f..48f97ace 100755 --- a/mmgen/proto/btc/tx/new.py +++ b/mmgen/proto/btc/tx/new.py @@ -73,8 +73,8 @@ class New(Base, TxNew): return fee_per_kb, fe_type # given tx size, rel fee and units, return absolute fee - def fee_rel2abs(self, tx_size, units, amt_in_units, unit): - return self.proto.coin_amt(amt_in_units * tx_size, from_unit=units[unit]) + def fee_rel2abs(self, tx_size, amt_in_units, unit): + return self.proto.coin_amt(int(amt_in_units * tx_size), from_unit=unit) # given network fee estimate in BTC/kB, return absolute fee using estimated tx size def fee_est2abs(self, fee_per_kb, *, fe_type=None): diff --git a/mmgen/proto/eth/tx/base.py b/mmgen/proto/eth/tx/base.py index 980fa92f..bb184d52 100755 --- a/mmgen/proto/eth/tx/base.py +++ b/mmgen/proto/eth/tx/base.py @@ -34,9 +34,8 @@ class Base(TxBase.Base): return self.outputs def pretty_fmt_fee(self, fee): - if fee < 1: - ret = f'{fee:.8f}'.rstrip('0') - return ret + '0' if ret.endswith('.') else ret + if fee < 10: + return f'{fee:.3f}'.rstrip('0').rstrip('.') return str(int(fee)) # given absolute fee in ETH, return gas price for display in selected unit diff --git a/mmgen/proto/eth/tx/new.py b/mmgen/proto/eth/tx/new.py index 117be9bc..e2b0c93c 100755 --- a/mmgen/proto/eth/tx/new.py +++ b/mmgen/proto/eth/tx/new.py @@ -126,8 +126,8 @@ class New(Base, TxBase.New): assert self.usr_fee <= self.proto.max_tx_fee # given rel fee and units, return absolute fee using self.gas - def fee_rel2abs(self, tx_size, units, amt_in_units, unit): - return self.proto.coin_amt(amt_in_units, from_unit=units[unit]) * self.gas.toWei() + def fee_rel2abs(self, tx_size, amt_in_units, unit): + return self.proto.coin_amt(int(amt_in_units * self.gas.toWei()), from_unit=unit) # given fee estimate (gas price) in wei, return absolute fee, adjusting by self.cfg.fee_adjust def fee_est2abs(self, rel_fee, *, fe_type=None): diff --git a/mmgen/tx/new.py b/mmgen/tx/new.py index 72b66407..c0fa06ab 100755 --- a/mmgen/tx/new.py +++ b/mmgen/tx/new.py @@ -70,6 +70,13 @@ def mmaddr2coinaddr(cfg, mmaddr, ad_w, ad_f, proto): return CoinAddr(proto, coin_addr) +def parse_fee_spec(proto, fee_arg): + import re + units = {u[0]:u for u in proto.coin_amt.units} + pat = re.compile(r'((?:[1-9][0-9]*)|(?:[0-9]+\.[0-9]+))({})'.format('|'.join(units))) + if m := pat.match(fee_arg): + return namedtuple('parsed_fee_spec', ['amt', 'unit'])(m[1], units[m[2]]) + class New(Base): fee_is_approximate = False @@ -117,12 +124,8 @@ class New(Base): if fee := get_obj(self.proto.coin_amt, num=fee_arg, silent=True): return fee - import re - units = {u[0]:u for u in self.proto.coin_amt.units} - pat = re.compile(r'([1-9][0-9]*)({})'.format('|'.join(units))) - if pat.match(fee_arg): - amt, unit = pat.match(fee_arg).groups() - return self.fee_rel2abs(tx_size, units, int(amt), unit) + if res := parse_fee_spec(self.proto, fee_arg): + return self.fee_rel2abs(tx_size, float(res.amt), res.unit) return False diff --git a/test/modtest_d/ut_amt.py b/test/modtest_d/ut_amt.py index 9cf236be..2b32f2b8 100755 --- a/test/modtest_d/ut_amt.py +++ b/test/modtest_d/ut_amt.py @@ -7,6 +7,7 @@ test.modtest_d.ut_amt: CoinAmt unit tests for the MMGen suite from decimal import Decimal from mmgen.protocol import init_proto +from mmgen.tx.new import parse_fee_spec from mmgen.cfg import Config from ..include.common import cfg, vmsg @@ -26,9 +27,18 @@ def test_to_unit(data): assert res == int(chk), f'{res} != {int(chk)}' return True +def test_fee_spec(data): + protos = get_protos(data) + for proto, spec, amt, unit in data: + vmsg(f' {proto.upper():6} {spec:<5} => {amt:<4} {unit}') + res = parse_fee_spec(protos[proto], spec) + assert res.amt == amt, f' {res.amt} != {amt}' + assert res.unit == unit, f' {res.unit} != {unit}' + return True + class unit_tests: - altcoin_deps = ('to_unit_alt',) + altcoin_deps = ('fee_spec_alt', 'to_unit_alt') def to_unit(self, name, ut, desc='CoinAmt.to_unit() (BTC)'): return test_to_unit(( @@ -54,3 +64,18 @@ class unit_tests: ('xmr', '1', 'atomic', '1000000000000'), ('xmr', '0.000000000001', 'atomic', '1'), ('xmr', '1.234567890123', 'atomic', '1234567890123'))) + + def fee_spec(self, name, ut, desc='fee spec parsing (BTC)'): + return test_fee_spec(( + ('btc', '32s', '32', 'satoshi'), + ('btc', '1s', '1', 'satoshi'))) + + def fee_spec_alt(self, name, ut, desc='fee spec parsing (LTC, BCH, ETH, XMR)'): + return test_fee_spec(( + ('ltc', '3.07s', '3.07', 'satoshi'), + ('bch', '3.07s', '3.07', 'satoshi'), + ('eth', '3.07G', '3.07', 'Gwei'), + ('eth', '37M', '37', 'Mwei'), + ('eth', '3701w', '3701', 'wei'), + ('eth', '3.07M', '3.07', 'Mwei'), + ('xmr', '3.07a', '3.07', 'atomic')))