From 384384c2605b27cd0690914265022553aac880b0 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Tue, 29 Sep 2026 09:23:31 +0000 Subject: [PATCH] rune: support user-specified transaction fees --- mmgen/amt.py | 2 +- mmgen/data/mmgen.cfg | 5 ++++- mmgen/main_txcreate.py | 1 + mmgen/proto/rune/params.py | 2 +- mmgen/proto/rune/tx/base.py | 4 ---- mmgen/proto/rune/tx/completed.py | 2 +- mmgen/proto/rune/tx/info.py | 6 +++++- mmgen/proto/rune/tx/new.py | 8 +++++++- mmgen/proto/rune/tx/protobuf.py | 24 ++++++++++++++++++------ mmgen/proto/rune/tx/signed.py | 3 ++- mmgen/proto/rune/tx/unsigned.py | 3 +++ mmgen/proto/vm/tx/new.py | 3 ++- test/cmdtest_d/rune.py | 4 +++- 13 files changed, 48 insertions(+), 19 deletions(-) diff --git a/mmgen/amt.py b/mmgen/amt.py index 1ce98ec3..844d3759 100755 --- a/mmgen/amt.py +++ b/mmgen/amt.py @@ -193,7 +193,7 @@ class BTCAmt(CoinAmt): max_amt = 21000000 satoshi = Decimal('0.00000001') atomic = satoshi - units = ('satoshi',) + units = ('atomic', 'satoshi') class UniAmt(BTCAmt): coin = None diff --git a/mmgen/data/mmgen.cfg b/mmgen/data/mmgen.cfg index c52fb0a2..59769cd2 100644 --- a/mmgen/data/mmgen.cfg +++ b/mmgen/data/mmgen.cfg @@ -133,7 +133,7 @@ ## rpc_host btc ltc bch eth etc ## ## rpc_port btc ltc bch eth etc xmr ## ## ignore_daemon_version btc ltc bch eth etc xmr ## -## max_tx_fee btc ltc bch eth etc ## +## max_tx_fee btc ltc bch eth etc rune ## ## Note: prefix is followed by an underscore, e.g. ‘xmr_rpc_port’ ## ####################################################################### ####################################################################### @@ -154,6 +154,9 @@ # Set the maximum transaction fee for ETH: # eth_max_tx_fee 0.005 +# Set the maximum transaction fee for RUNE: +# rune_max_tx_fee 0.2 + # Set the Ethereum mainnet chain names (space-separated list, first is default): # eth_mainnet_chain_names ethereum foundation diff --git a/mmgen/main_txcreate.py b/mmgen/main_txcreate.py index 8c48de4e..7db17f24 100755 --- a/mmgen/main_txcreate.py +++ b/mmgen/main_txcreate.py @@ -55,6 +55,7 @@ opts_data = { e- -D, --contract-data=D Path to file containing hex-encoded contract data b- -E, --fee-estimate-mode=M Specify the network fee estimate mode. Choices: + {fe_all}. Default: {fe_dfl!r} + X- -f, --fee= f Transaction fee, as a decimal {cu} amount R- -f, --fee= f Transaction fee, as a decimal {cu} amount or as + {fu} (an integer followed by {fl}). + See FEE SPECIFICATION below. If omitted, fee will be diff --git a/mmgen/proto/rune/params.py b/mmgen/proto/rune/params.py index a4da6192..09137d19 100755 --- a/mmgen/proto/rune/params.py +++ b/mmgen/proto/rune/params.py @@ -27,7 +27,7 @@ class mainnet(CoinProtocol.Secp256k1): preferred_mmtypes = ('X',) dfl_mmtype = 'X' coin_amt = 'UniAmt' - max_tx_fee = 0.1 + max_tx_fee = 0.2 caps = () mmcaps = ('tw', 'rpc', 'rpc_init') base_proto = 'THORChain' diff --git a/mmgen/proto/rune/tx/base.py b/mmgen/proto/rune/tx/base.py index 7fc984ae..e2850dc4 100755 --- a/mmgen/proto/rune/tx/base.py +++ b/mmgen/proto/rune/tx/base.py @@ -25,10 +25,6 @@ class Base(TxBase): def nondata_outputs(self): return self.outputs - @property - def usr_fee(self): - return self.proto.coin_amt(self.dfl_fee, from_unit='satoshi') - def add_blockcount(self): pass diff --git a/mmgen/proto/rune/tx/completed.py b/mmgen/proto/rune/tx/completed.py index 43d9b845..1c766151 100755 --- a/mmgen/proto/rune/tx/completed.py +++ b/mmgen/proto/rune/tx/completed.py @@ -26,4 +26,4 @@ class Completed(VmCompleted, Base, TxBase.Completed): @property def fee(self): - return self.proto.coin_amt(self.dfl_fee, from_unit='satoshi') + return self.txobj['fee'] if self.txobj['fee'] else self.proto.coin_amt('0') diff --git a/mmgen/proto/rune/tx/info.py b/mmgen/proto/rune/tx/info.py index 722a1406..c3b4cb97 100755 --- a/mmgen/proto/rune/tx/info.py +++ b/mmgen/proto/rune/tx/info.py @@ -13,7 +13,7 @@ proto.rune.tx.info: THORChain transaction info class """ from ....tx.info import TxInfo -from ....color import pink +from ....color import blue, pink from ....obj import NonNegativeInt from ...vm.tx.info import VmTxInfo, mmid_disp @@ -27,6 +27,7 @@ class TxInfo(VmTxInfo, TxInfo): fs = """ From: {f}{f_mmid} Amount: {a} {c} + Fee: {F} {C} Gas limit: {G} Sequence: {N} Memo: {m} @@ -34,6 +35,7 @@ class TxInfo(VmTxInfo, TxInfo): From: {f}{f_mmid} To: {t}{t_mmid} Amount: {a} {c} + Fee: {F} {C} Gas limit: {G} Sequence: {N} """ @@ -41,9 +43,11 @@ class TxInfo(VmTxInfo, TxInfo): f = t['from'].hl(0), t = None if has_memo else t['to'].hl(0), a = t['amt'].hl(), + F = blue('None') if t['fee'] is None else t['fee'].hl(), N = NonNegativeInt(t['sequence']).hl(), m = pink(tx.swap_memo) if has_memo else None, c = tx.proto.dcoin if tx.outputs else '', + C = tx.proto.dcoin, G = NonNegativeInt(tx.total_gas).hl(), f_mmid = mmid_disp(tx.inputs[0], nonmm_str), t_mmid = None if has_memo else mmid_disp(tx.outputs[0], nonmm_str)) + '\n\n' diff --git a/mmgen/proto/rune/tx/new.py b/mmgen/proto/rune/tx/new.py index 2c04aca4..790f3ee4 100755 --- a/mmgen/proto/rune/tx/new.py +++ b/mmgen/proto/rune/tx/new.py @@ -20,8 +20,13 @@ from .base import Base class New(VmNew, Base, TxBase.New): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.usr_fee = None if self.cfg.fee is None else self.proto.coin_amt(self.cfg.fee) + async def get_funds_remaining(self, fee, outputs_sum, start_fee_desc): - return await self.twctl.get_balance(self.inputs[0].addr) + res = await self.twctl.get_balance(self.inputs[0].addr) + return (res - self.usr_fee) if self.usr_fee else res async def set_gas(self, *, to_addr=None, force=False): self.gas = self.dfl_gas @@ -40,6 +45,7 @@ class New(VmNew, Base, TxBase.New): 'from': self.inputs[0].addr, 'to': self.outputs[0].addr if self.outputs else None, 'amt': self.sum_outputs(), + 'fee': self.usr_fee, 'gas': self.gas, 'account_number': int(acct_info['account_number']), 'sequence': int(acct_info['sequence']), diff --git a/mmgen/proto/rune/tx/protobuf.py b/mmgen/proto/rune/tx/protobuf.py index 8d619575..b458b32a 100755 --- a/mmgen/proto/rune/tx/protobuf.py +++ b/mmgen/proto/rune/tx/protobuf.py @@ -44,10 +44,11 @@ send_tx_parms = namedtuple( 'gas_limit', 'account_number', 'sequence', + 'fee', # begin default fields 'pubkey', 'wifkey', 'signature'], - defaults = (None, None, None)) + defaults = (0, None, None, None)) deposit_tx_parms = namedtuple( 'rune_deposit_tx_parms', [ @@ -61,13 +62,14 @@ deposit_tx_parms = namedtuple( 'sequence', 'decimals', 'memo', - 'synth', # begin default fields + 'fee', # begin default fields + 'synth', 'trade', 'secured', 'pubkey', 'wifkey', 'signature'], - defaults = (None, None, None, None, None, None)) + defaults = (0, None, None, None, None, None, None)) # subset of deposit_tx_parms: swap_tx_parms = namedtuple( @@ -78,13 +80,14 @@ swap_tx_parms = namedtuple( 'account_number', 'sequence', 'memo', - 'synth', # begin default fields + 'fee', # begin default fields + 'synth', 'trade', 'secured', 'pubkey', 'wifkey', 'signature'], - defaults = (None, None, None, None, None, None)) + defaults = (0, None, None, None, None, None, None)) @dataclass class Asset(BaseMessage): @@ -158,9 +161,16 @@ def amt_to_base_unit(amt, *, decimals): def base_unit_to_amt(n, *, decimals): return n * Decimal(10) ** -decimals +def parse_fee(proto, tx, color=None): + if (fee := tx.authInfo.fee.amount) is None: + return None + else: + return proto.coin_amt(int(fee[0].amount), from_unit='atomic') + def tx_info(tx, proto): b = tx.body.messages[0].body s = tx.authInfo.signerInfos[0] + fee = parse_fee(proto, tx) match msg_type := tx.body.messages[0].id.removeprefix('/types.'): case 'MsgSend': from_addr = proto.encode_addr_bech32x(b.fromAddress) @@ -180,6 +190,7 @@ def tx_info(tx, proto): yield f'To: {to_addr}' yield f'Asset: {asset}' yield f'Amount: {amt}' + yield f'Fee: {None if fee is None else fee.hl(color=None)}' yield f'Sequence: {int(s.sequence)}' yield f'Gas limit: {tx.authInfo.fee.gasLimit}' yield f'Memo: {memo}' @@ -203,6 +214,7 @@ def build_swap_tx(cfg, proto, parms, *, skip_body_memo=False): account_number = p.account_number, sequence = p.sequence, decimals = 8, + fee = p.fee, synth = p.synth, trade = p.trade, secured = p.secured, @@ -256,7 +268,7 @@ def build_tx(cfg, proto, parms, *, null_fee=False, skip_body_memo=False): coins = [coin_data], memo = p.memo, signer = proto.decode_addr(p.from_addr).bytes)) - fee_amt = None if null_fee else [Coin(denom='rune', amount='0')] + fee_amt = None if null_fee else [Coin(denom='rune', amount=str(p.fee))] signer_info = SignerInfo( publicKey = PublicKey( diff --git a/mmgen/proto/rune/tx/signed.py b/mmgen/proto/rune/tx/signed.py index 0b2e290a..50cffd20 100755 --- a/mmgen/proto/rune/tx/signed.py +++ b/mmgen/proto/rune/tx/signed.py @@ -21,7 +21,7 @@ class Signed(Completed, TxBase.Signed): desc = 'signed transaction' def parse_txfile_serialized_data(self): - from .protobuf import RuneTx + from .protobuf import RuneTx, parse_fee tx = RuneTx.loads(bytes.fromhex(self.serialized)) b = tx.body.messages[0].body @@ -33,6 +33,7 @@ class Signed(Completed, TxBase.Signed): 'from': self.proto.encode_addr_bech32x(getattr(b, from_k)), 'to': None if self.is_swap else self.proto.encode_addr_bech32x(b.toAddress), 'amt': self.proto.coin_amt(int(getattr(b, amt_k)[0].amount), from_unit='satoshi'), + 'fee': parse_fee(self.proto, tx), 'gas': NonNegativeInt(i.fee.gasLimit), 'sequence': NonNegativeInt(i.signerInfos[0].sequence)} diff --git a/mmgen/proto/rune/tx/unsigned.py b/mmgen/proto/rune/tx/unsigned.py index e5981cf0..741f3bab 100755 --- a/mmgen/proto/rune/tx/unsigned.py +++ b/mmgen/proto/rune/tx/unsigned.py @@ -28,6 +28,7 @@ class Unsigned(VmUnsigned, Completed, TxBase.Unsigned): 'from': CoinAddr(self.proto, d['from']), 'to': CoinAddr(self.proto, d['to']) if d['to'] else None, 'amt': self.proto.coin_amt(d['amt']), + 'fee': None if d['fee'] is None else self.proto.coin_amt(d['fee']), 'gas': NonNegativeInt(d['gas']), 'account_number': NonNegativeInt(d['account_number']), 'sequence': NonNegativeInt(d['sequence']), @@ -43,6 +44,7 @@ class Unsigned(VmUnsigned, Completed, TxBase.Unsigned): o['account_number'], o['sequence'], self.swap_memo, + fee = 0 if o['fee'] is None else o['fee'].to_unit('atomic'), wifkey = wif) else: from .protobuf import send_tx_parms, build_tx @@ -53,6 +55,7 @@ class Unsigned(VmUnsigned, Completed, TxBase.Unsigned): o['gas'], o['account_number'], o['sequence'], + fee = 0 if o['fee'] is None else o['fee'].to_unit('atomic'), wifkey = wif) tx = build_tx(self.cfg, self.proto, parms) diff --git a/mmgen/proto/vm/tx/new.py b/mmgen/proto/vm/tx/new.py index 965d75b0..33ba62fc 100755 --- a/mmgen/proto/vm/tx/new.py +++ b/mmgen/proto/vm/tx/new.py @@ -86,7 +86,8 @@ class New: def check_fee(self): if not self.disable_fee_check: - assert self.usr_fee <= self.proto.max_tx_fee + if self.usr_fee and self.usr_fee > self.proto.max_tx_fee: + die('MaxFeeExceeded', f'{self.usr_fee} > max fee ({self.proto.max_tx_fee})') @property def total_gas(self): diff --git a/test/cmdtest_d/rune.py b/test/cmdtest_d/rune.py index aac48336..7d50fa43 100755 --- a/test/cmdtest_d/rune.py +++ b/test/cmdtest_d/rune.py @@ -139,8 +139,10 @@ class CmdTestRune(CmdTestRuneMethods, CmdTestEthdevMethods, CmdTestBase, CmdTest def twview(self): return self.spawn('mmgen-tool', self.rune_opts + self.add_eth_opts + ['twview']) + def txcreate1(self): + return self._rune_txcreate(add_opts=['--fee=0.2']) + bal_refresh = CmdTestRuneMethods._rune_bal_refresh - txcreate1 = CmdTestRuneMethods._rune_txcreate txsign1 = CmdTestRuneMethods._rune_txsign _txsend = CmdTestRuneMethods._rune_txsend