rune: support user-specified transaction fees

This commit is contained in:
The MMGen Project 2026-09-29 09:23:31 +00:00
commit 384384c260
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
13 changed files with 48 additions and 19 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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')

View file

@ -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'

View file

@ -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']),

View file

@ -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(

View file

@ -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)}

View file

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

View file

@ -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):

View file

@ -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