new proto.vm.tx package
This commit is contained in:
parent
55e59cee12
commit
3ad310e259
7 changed files with 234 additions and 173 deletions
|
|
@ -13,18 +13,14 @@ proto.eth.tx.completed: Ethereum completed transaction class
|
|||
"""
|
||||
|
||||
from ....tx import completed as TxBase
|
||||
|
||||
from ...vm.tx.completed import Completed as VmCompleted
|
||||
|
||||
from .base import Base, TokenBase
|
||||
|
||||
class Completed(Base, TxBase.Completed):
|
||||
class Completed(VmCompleted, Base, TxBase.Completed):
|
||||
fn_fee_unit = 'Mwei'
|
||||
|
||||
def get_swap_memo_maybe(self):
|
||||
return self.swap_memo.encode() if getattr(self, 'swap_memo', None) else None
|
||||
|
||||
@property
|
||||
def send_amt(self):
|
||||
return self.outputs[0].amt if self.outputs else self.proto.coin_amt('0')
|
||||
|
||||
@property
|
||||
def total_gas(self):
|
||||
return self.txobj['startGas']
|
||||
|
|
@ -33,25 +29,6 @@ class Completed(Base, TxBase.Completed):
|
|||
def fee(self):
|
||||
return self.fee_gasPrice2abs(self.txobj['gasPrice'].toWei())
|
||||
|
||||
@property
|
||||
def change(self):
|
||||
return self.sum_inputs() - self.send_amt - self.fee
|
||||
|
||||
def check_txfile_hex_data(self):
|
||||
pass
|
||||
|
||||
def check_sigs(self): # TODO
|
||||
from ....util import is_hex_str
|
||||
if is_hex_str(self.serialized):
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_pubkey_scripts(self):
|
||||
pass
|
||||
|
||||
def get_serialized_locktime(self):
|
||||
return None # TODO
|
||||
|
||||
class TokenCompleted(TokenBase, Completed):
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -13,19 +13,18 @@ proto.eth.tx.new: Ethereum new transaction class
|
|||
"""
|
||||
|
||||
from ....tx import new as TxBase
|
||||
from ....obj import Int, ETHNonce, MMGenTxID
|
||||
from ....util import msg, ymsg, is_int, is_hex_str, make_chksum_6, suf, die
|
||||
from ....tw.ctl import TwCtl
|
||||
from ....addr import is_mmgen_id, is_coin_addr
|
||||
from ....obj import Int, ETHNonce
|
||||
from ....util import msg, ymsg, is_int
|
||||
|
||||
from ...vm.tx.new import New as VmNew
|
||||
|
||||
from ..contract import Token
|
||||
|
||||
from .base import Base, TokenBase
|
||||
|
||||
class New(Base, TxBase.New):
|
||||
desc = 'transaction'
|
||||
class New(VmNew, Base, TxBase.New):
|
||||
fee_fail_fs = 'Network fee estimation failed'
|
||||
no_chg_msg = 'Warning: Transaction leaves account with zero balance'
|
||||
usr_fee_prompt = 'Enter transaction fee or gas price: '
|
||||
msg_insufficient_funds = 'Account balance insufficient to fund this transaction ({} {} needed)'
|
||||
byte_cost = 68 # https://ethereum.stackexchange.com/questions/39401/
|
||||
# how-do-you-calculate-gas-limit-for-transaction-with-data-in-ethereum
|
||||
|
||||
|
|
@ -70,24 +69,6 @@ class New(Base, TxBase.New):
|
|||
'chainId': self.rpc.chainID,
|
||||
'data': self.usr_contract_data.hex()}
|
||||
|
||||
# Instead of serializing tx data as with BTC, just create a JSON dump.
|
||||
# This complicates things but means we avoid using the rlp library to deserialize the data,
|
||||
# thus removing an attack vector
|
||||
async def create_serialized(self, *, locktime=None):
|
||||
assert len(self.inputs) == 1, 'Transaction has more than one input!'
|
||||
o_num = len(self.outputs)
|
||||
o_ok = 0 if self.usr_contract_data else 1
|
||||
assert o_num == o_ok, f'Transaction has {o_num} output{suf(o_num)} (should have {o_ok})'
|
||||
await self.make_txobj()
|
||||
self.serialized = {k:v if v is None else str(v) for k, v in self.txobj.items() if k != 'token_to'}
|
||||
self.update_txid()
|
||||
|
||||
def update_txid(self):
|
||||
import json
|
||||
assert not is_hex_str(self.serialized), (
|
||||
'update_txid() must be called only when self.serialized is not hex data')
|
||||
self.txid = MMGenTxID(make_chksum_6(json.dumps(self.serialized)).upper())
|
||||
|
||||
def set_gas_with_data(self, data):
|
||||
if not self.is_token:
|
||||
self.gas = self.dfl_gas + self.byte_cost * len(data)
|
||||
|
|
@ -98,47 +79,6 @@ class New(Base, TxBase.New):
|
|||
self.gas += self.byte_cost * extra_data_len
|
||||
self._gas_adjusted = True
|
||||
|
||||
async def process_cmdline_args(self, cmd_args, ad_f, ad_w):
|
||||
|
||||
lc = len(cmd_args)
|
||||
|
||||
if lc == 2 and self.is_swap:
|
||||
data_arg = cmd_args.pop()
|
||||
lc = 1
|
||||
assert data_arg.startswith('data:'), f'{data_arg}: invalid data arg (must start with "data:")'
|
||||
self.swap_memo = data_arg.removeprefix('data:')
|
||||
self.set_gas_with_data(self.swap_memo.encode())
|
||||
|
||||
if lc == 0 and self.usr_contract_data and 'Token' not in self.name:
|
||||
return
|
||||
|
||||
if lc != 1:
|
||||
die(1, f'{lc} output{suf(lc)} specified, but Ethereum transactions must have exactly one')
|
||||
|
||||
a = self.parse_cmdline_arg(self.proto, cmd_args[0], ad_f, ad_w)
|
||||
|
||||
self.add_output(
|
||||
coinaddr = None if a.is_vault else a.addr,
|
||||
amt = self.proto.coin_amt(a.amt or '0'),
|
||||
is_chg = not a.amt,
|
||||
is_vault = a.is_vault)
|
||||
|
||||
self.add_mmaddrs_to_outputs(ad_f, ad_w)
|
||||
|
||||
def get_unspent_nums_from_user(self, unspent):
|
||||
from ....ui import line_input
|
||||
while True:
|
||||
reply = line_input(self.cfg, 'Enter an account to spend from: ').strip()
|
||||
if reply:
|
||||
if not is_int(reply):
|
||||
msg('Account number must be an integer')
|
||||
elif int(reply) < 1:
|
||||
msg('Account number must be >= 1')
|
||||
elif int(reply) > len(unspent):
|
||||
msg(f'Account number must be <= {len(unspent)}')
|
||||
else:
|
||||
return [int(reply)]
|
||||
|
||||
@property
|
||||
def network_estimated_fee_label(self):
|
||||
return 'Network-estimated'
|
||||
|
|
@ -153,17 +93,6 @@ class New(Base, TxBase.New):
|
|||
Int(await self.rpc.call('eth_gasPrice'), base=16),
|
||||
'eth_gasPrice')
|
||||
|
||||
def check_chg_addr_is_wallet_addr(self):
|
||||
pass
|
||||
|
||||
def check_fee(self):
|
||||
if not self.disable_fee_check:
|
||||
assert self.usr_fee <= self.proto.max_tx_fee
|
||||
|
||||
@property
|
||||
def total_gas(self):
|
||||
return self.gas
|
||||
|
||||
# given rel fee and units, return absolute fee using self.total_gas
|
||||
def fee_rel2abs(self, tx_size, amt_in_units, unit):
|
||||
return self.proto.coin_amt(int(amt_in_units * self.total_gas), from_unit=unit)
|
||||
|
|
@ -189,35 +118,6 @@ class New(Base, TxBase.New):
|
|||
else:
|
||||
return abs_fee
|
||||
|
||||
def update_change_output(self, funds_left):
|
||||
if self.outputs and self.outputs[0].is_chg:
|
||||
self.update_output_amt(0, funds_left)
|
||||
|
||||
async def get_input_addrs_from_inputs_opt(self):
|
||||
ret = []
|
||||
if self.cfg.inputs:
|
||||
data_root = (await TwCtl(self.cfg, self.proto)).data_root # must create new instance here
|
||||
errmsg = 'Address {!r} not in tracking wallet'
|
||||
for addr in self.cfg.inputs.split(','):
|
||||
if is_mmgen_id(self.proto, addr):
|
||||
for waddr in data_root:
|
||||
if data_root[waddr]['mmid'] == addr:
|
||||
ret.append(waddr)
|
||||
break
|
||||
else:
|
||||
die('UserAddressNotInWallet', errmsg.format(addr))
|
||||
elif is_coin_addr(self.proto, addr):
|
||||
if not addr in data_root:
|
||||
die('UserAddressNotInWallet', errmsg.format(addr))
|
||||
ret.append(addr)
|
||||
else:
|
||||
die(1, f'{addr!r}: not an MMGen ID or coin address')
|
||||
return ret
|
||||
|
||||
def final_inputs_ok_msg(self, funds_left):
|
||||
chg = self.proto.coin_amt('0') if (self.outputs and self.outputs[0].is_chg) else funds_left
|
||||
return 'Transaction leaves {} {} in the sender’s account'.format(chg.hl(), self.proto.coin)
|
||||
|
||||
class TokenNew(TokenBase, New):
|
||||
desc = 'transaction'
|
||||
fee_is_approximate = True
|
||||
|
|
|
|||
|
|
@ -15,14 +15,16 @@ proto.eth.tx.unsigned: Ethereum unsigned transaction class
|
|||
import json
|
||||
|
||||
from ....tx import unsigned as TxBase
|
||||
from ....util import msg, msg_r, die
|
||||
from ....obj import CoinTxID, ETHNonce, Int, HexStr
|
||||
from ....addr import CoinAddr, ContractAddr
|
||||
|
||||
from ...vm.tx.unsigned import Unsigned as VmUnsigned
|
||||
|
||||
from ..contract import Token, THORChainRouterContract
|
||||
|
||||
from .completed import Completed, TokenCompleted
|
||||
|
||||
class Unsigned(Completed, TxBase.Unsigned):
|
||||
desc = 'unsigned transaction'
|
||||
class Unsigned(VmUnsigned, Completed, TxBase.Unsigned):
|
||||
|
||||
def parse_txfile_serialized_data(self):
|
||||
d = self.serialized if isinstance(self.serialized, dict) else json.loads(self.serialized)
|
||||
|
|
@ -66,42 +68,6 @@ class Unsigned(Completed, TxBase.Unsigned):
|
|||
else:
|
||||
self.txobj['token_addr'] = ContractAddr(self.proto, etx.creates.hex())
|
||||
|
||||
async def sign(self, tx_num_str, keys): # return TX object or False; don't exit or raise exception
|
||||
|
||||
from ....exception import TransactionChainMismatch
|
||||
try:
|
||||
self.check_correct_chain()
|
||||
except TransactionChainMismatch:
|
||||
return False
|
||||
|
||||
o = self.txobj
|
||||
|
||||
def do_mismatch_err(io, j, k, desc):
|
||||
m = 'A compromised online installation may have altered your serialized data!'
|
||||
fs = '\n{} mismatch!\n{}\n orig: {}\n serialized: {}'
|
||||
die(3, fs.format(desc.upper(), m, getattr(io[0], k), o[j]))
|
||||
|
||||
if o['from'] != self.inputs[0].addr:
|
||||
do_mismatch_err(self.inputs, 'from', 'addr', 'from-address')
|
||||
if self.outputs:
|
||||
if o['to'] != self.outputs[0].addr:
|
||||
do_mismatch_err(self.outputs, 'to', 'addr', 'to-address')
|
||||
if o['amt'] != self.outputs[0].amt:
|
||||
do_mismatch_err(self.outputs, 'amt', 'amt', 'amount')
|
||||
|
||||
msg_r(f'Signing transaction{tx_num_str}...')
|
||||
|
||||
try:
|
||||
await self.do_sign(o, keys[0].sec.wif)
|
||||
msg('OK')
|
||||
from ....tx import SignedTX
|
||||
tx = SignedTX(cfg=self.cfg, data=self.__dict__, automount=self.automount)
|
||||
tx.check_serialized_integrity()
|
||||
return tx
|
||||
except Exception as e:
|
||||
msg(f'{e}: transaction signing failed!')
|
||||
return False
|
||||
|
||||
class TokenUnsigned(TokenCompleted, Unsigned):
|
||||
desc = 'unsigned transaction'
|
||||
|
||||
|
|
|
|||
41
mmgen/proto/vm/tx/completed.py
Executable file
41
mmgen/proto/vm/tx/completed.py
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# MMGen Wallet, a terminal-based cryptocurrency wallet
|
||||
# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen-wallet
|
||||
# https://gitlab.com/mmgen/mmgen-wallet
|
||||
|
||||
"""
|
||||
proto.vm.tx.completed: completed transaction methods for VM chains
|
||||
"""
|
||||
|
||||
class Completed:
|
||||
|
||||
def get_swap_memo_maybe(self):
|
||||
return self.swap_memo.encode() if getattr(self, 'swap_memo', None) else None
|
||||
|
||||
@property
|
||||
def send_amt(self):
|
||||
return self.outputs[0].amt if self.outputs else self.proto.coin_amt('0')
|
||||
|
||||
@property
|
||||
def change(self):
|
||||
return self.sum_inputs() - self.send_amt - self.fee
|
||||
|
||||
def check_txfile_hex_data(self):
|
||||
pass
|
||||
|
||||
def check_sigs(self): # TODO
|
||||
from ....util import is_hex_str
|
||||
if is_hex_str(self.serialized):
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_pubkey_scripts(self):
|
||||
pass
|
||||
|
||||
def get_serialized_locktime(self):
|
||||
return None # TODO
|
||||
121
mmgen/proto/vm/tx/new.py
Executable file
121
mmgen/proto/vm/tx/new.py
Executable file
|
|
@ -0,0 +1,121 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# MMGen Wallet, a terminal-based cryptocurrency wallet
|
||||
# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen-wallet
|
||||
# https://gitlab.com/mmgen/mmgen-wallet
|
||||
|
||||
"""
|
||||
proto.vm.tx.new: new transaction class methods for VM chains
|
||||
"""
|
||||
|
||||
from ....obj import MMGenTxID
|
||||
from ....util import msg, is_int, is_hex_str, make_chksum_6, suf, die
|
||||
from ....tw.ctl import TwCtl
|
||||
from ....addr import is_mmgen_id, is_coin_addr
|
||||
|
||||
class New:
|
||||
desc = 'transaction'
|
||||
no_chg_msg = 'Warning: Transaction leaves account with zero balance'
|
||||
msg_insufficient_funds = 'Account balance insufficient to fund this transaction ({} {} needed)'
|
||||
|
||||
# Instead of serializing tx data online as with BTC, put the data in a dict and serialize
|
||||
# offline before signing
|
||||
async def create_serialized(self, *, locktime=None):
|
||||
assert len(self.inputs) == 1, 'Transaction has more than one input!'
|
||||
o_num = len(self.outputs)
|
||||
o_ok = 0 if self.usr_contract_data else 1
|
||||
assert o_num == o_ok, f'Transaction has {o_num} output{suf(o_num)} (should have {o_ok})'
|
||||
await self.make_txobj()
|
||||
self.serialized = {k:v if v is None else str(v) for k, v in self.txobj.items() if k != 'token_to'}
|
||||
self.update_txid()
|
||||
|
||||
def update_txid(self):
|
||||
import json
|
||||
assert not is_hex_str(self.serialized), (
|
||||
'update_txid() must be called only when self.serialized is not hex data')
|
||||
self.txid = MMGenTxID(make_chksum_6(json.dumps(self.serialized)).upper())
|
||||
|
||||
async def process_cmdline_args(self, cmd_args, ad_f, ad_w):
|
||||
|
||||
lc = len(cmd_args)
|
||||
|
||||
if lc == 2 and self.is_swap:
|
||||
data_arg = cmd_args.pop()
|
||||
lc = 1
|
||||
assert data_arg.startswith('data:'), f'{data_arg}: invalid data arg (must start with "data:")'
|
||||
self.swap_memo = data_arg.removeprefix('data:')
|
||||
self.set_gas_with_data(self.swap_memo.encode())
|
||||
|
||||
if lc == 0 and self.usr_contract_data and 'Token' not in self.name:
|
||||
return
|
||||
|
||||
if lc != 1:
|
||||
die(1, f'{lc} output{suf(lc)} specified, but VM transactions must have exactly one')
|
||||
|
||||
a = self.parse_cmdline_arg(self.proto, cmd_args[0], ad_f, ad_w)
|
||||
|
||||
self.add_output(
|
||||
coinaddr = None if a.is_vault else a.addr,
|
||||
amt = self.proto.coin_amt(a.amt or '0'),
|
||||
is_chg = not a.amt,
|
||||
is_vault = a.is_vault)
|
||||
|
||||
self.add_mmaddrs_to_outputs(ad_f, ad_w)
|
||||
|
||||
def get_unspent_nums_from_user(self, unspent):
|
||||
from ....ui import line_input
|
||||
while True:
|
||||
reply = line_input(self.cfg, 'Enter an account to spend from: ').strip()
|
||||
if reply:
|
||||
if not is_int(reply):
|
||||
msg('Account number must be an integer')
|
||||
elif int(reply) < 1:
|
||||
msg('Account number must be >= 1')
|
||||
elif int(reply) > len(unspent):
|
||||
msg(f'Account number must be <= {len(unspent)}')
|
||||
else:
|
||||
return [int(reply)]
|
||||
|
||||
def check_chg_addr_is_wallet_addr(self):
|
||||
pass
|
||||
|
||||
def check_fee(self):
|
||||
if not self.disable_fee_check:
|
||||
assert self.usr_fee <= self.proto.max_tx_fee
|
||||
|
||||
@property
|
||||
def total_gas(self):
|
||||
return self.gas
|
||||
|
||||
def update_change_output(self, funds_left):
|
||||
if self.outputs and self.outputs[0].is_chg:
|
||||
self.update_output_amt(0, funds_left)
|
||||
|
||||
async def get_input_addrs_from_inputs_opt(self):
|
||||
ret = []
|
||||
if self.cfg.inputs:
|
||||
data_root = (await TwCtl(self.cfg, self.proto)).data_root # must create new instance here
|
||||
errmsg = 'Address {!r} not in tracking wallet'
|
||||
for addr in self.cfg.inputs.split(','):
|
||||
if is_mmgen_id(self.proto, addr):
|
||||
for waddr in data_root:
|
||||
if data_root[waddr]['mmid'] == addr:
|
||||
ret.append(waddr)
|
||||
break
|
||||
else:
|
||||
die('UserAddressNotInWallet', errmsg.format(addr))
|
||||
elif is_coin_addr(self.proto, addr):
|
||||
if not addr in data_root:
|
||||
die('UserAddressNotInWallet', errmsg.format(addr))
|
||||
ret.append(addr)
|
||||
else:
|
||||
die(1, f'{addr!r}: not an MMGen ID or coin address')
|
||||
return ret
|
||||
|
||||
def final_inputs_ok_msg(self, funds_left):
|
||||
chg = self.proto.coin_amt('0') if (self.outputs and self.outputs[0].is_chg) else funds_left
|
||||
return 'Transaction leaves {} {} in the sender’s account'.format(chg.hl(), self.proto.coin)
|
||||
54
mmgen/proto/vm/tx/unsigned.py
Executable file
54
mmgen/proto/vm/tx/unsigned.py
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# MMGen Wallet, a terminal-based cryptocurrency wallet
|
||||
# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
|
||||
# Licensed under the GNU General Public License, Version 3:
|
||||
# https://www.gnu.org/licenses
|
||||
# Public project repositories:
|
||||
# https://github.com/mmgen/mmgen-wallet
|
||||
# https://gitlab.com/mmgen/mmgen-wallet
|
||||
|
||||
"""
|
||||
proto.vm.tx.unsigned: unsigned transaction methods for VM chains
|
||||
"""
|
||||
|
||||
from ....util import msg, msg_r, die
|
||||
|
||||
class Unsigned:
|
||||
desc = 'unsigned transaction'
|
||||
|
||||
async def sign(self, tx_num_str, keys): # return TX object or False; don't exit or raise exception
|
||||
|
||||
from ....exception import TransactionChainMismatch
|
||||
try:
|
||||
self.check_correct_chain()
|
||||
except TransactionChainMismatch:
|
||||
return False
|
||||
|
||||
o = self.txobj
|
||||
|
||||
def do_mismatch_err(io, j, k, desc):
|
||||
m = 'A compromised online installation may have altered your serialized data!'
|
||||
fs = '\n{} mismatch!\n{}\n orig: {}\n serialized: {}'
|
||||
die(3, fs.format(desc.upper(), m, getattr(io[0], k), o[j]))
|
||||
|
||||
if o['from'] != self.inputs[0].addr:
|
||||
do_mismatch_err(self.inputs, 'from', 'addr', 'from-address')
|
||||
if self.outputs:
|
||||
if o['to'] != self.outputs[0].addr:
|
||||
do_mismatch_err(self.outputs, 'to', 'addr', 'to-address')
|
||||
if o['amt'] != self.outputs[0].amt:
|
||||
do_mismatch_err(self.outputs, 'amt', 'amt', 'amount')
|
||||
|
||||
msg_r(f'Signing transaction{tx_num_str}...')
|
||||
|
||||
try:
|
||||
await self.do_sign(o, keys[0].sec.wif)
|
||||
msg('OK')
|
||||
from ....tx import SignedTX
|
||||
tx = SignedTX(cfg=self.cfg, data=self.__dict__, automount=self.automount)
|
||||
tx.check_serialized_integrity()
|
||||
return tx
|
||||
except Exception as e:
|
||||
msg(f'{e}: transaction signing failed!')
|
||||
return False
|
||||
|
|
@ -100,6 +100,8 @@ packages =
|
|||
mmgen.proto.rune.tw
|
||||
mmgen.proto.rune.tx
|
||||
mmgen.proto.secp256k1
|
||||
mmgen.proto.vm
|
||||
mmgen.proto.vm.tx
|
||||
mmgen.proto.xchain
|
||||
mmgen.proto.xmr
|
||||
mmgen.proto.zec
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue