unsigned.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
  5. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen-wallet
  9. # https://gitlab.com/mmgen/mmgen-wallet
  10. """
  11. proto.eth.tx.unsigned: Ethereum unsigned transaction class
  12. """
  13. import json
  14. from ....tx import unsigned as TxBase
  15. from ....util import msg, msg_r, die
  16. from ....obj import CoinTxID, ETHNonce, Int, HexStr
  17. from ....addr import CoinAddr, TokenAddr
  18. from ..contract import Token
  19. from .completed import Completed, TokenCompleted
  20. class Unsigned(Completed, TxBase.Unsigned):
  21. desc = 'unsigned transaction'
  22. def parse_txfile_serialized_data(self):
  23. d = json.loads(self.serialized)
  24. o = {
  25. 'from': CoinAddr(self.proto, d['from']),
  26. # NB: for token, 'to' is sendto address
  27. 'to': CoinAddr(self.proto, d['to']) if d['to'] else None,
  28. 'amt': self.proto.coin_amt(d['amt']),
  29. 'gasPrice': self.proto.coin_amt(d['gasPrice']),
  30. 'startGas': self.proto.coin_amt(d['startGas']),
  31. 'nonce': ETHNonce(d['nonce']),
  32. 'chainId': None if d['chainId'] == 'None' else Int(d['chainId']),
  33. 'data': HexStr(d['data'])}
  34. self.gas = o['startGas']
  35. self.txobj = o
  36. return d # 'token_addr', 'decimals' required by Token subclass
  37. async def do_sign(self, o, wif):
  38. o_conv = {
  39. 'to': bytes.fromhex(o['to'] or ''),
  40. 'startgas': o['startGas'].toWei(),
  41. 'gasprice': o['gasPrice'].toWei(),
  42. 'value': o['amt'].toWei() if o['amt'] else 0,
  43. 'nonce': o['nonce'],
  44. 'data': bytes.fromhex(o['data'])}
  45. from ..pyethereum.transactions import Transaction
  46. etx = Transaction(**o_conv).sign(wif, o['chainId'])
  47. assert etx.sender.hex() == o['from'], (
  48. 'Sender address recovered from signature does not match true sender')
  49. from .. import rlp
  50. self.serialized = rlp.encode(etx).hex()
  51. self.coin_txid = CoinTxID(etx.hash.hex())
  52. if o['data']: # contract-creating transaction
  53. if o['to']:
  54. if not self.is_swap:
  55. raise ValueError('contract-creating transaction cannot have to-address')
  56. else:
  57. self.txobj['token_addr'] = TokenAddr(self.proto, etx.creates.hex())
  58. async def sign(self, tx_num_str, keys): # return TX object or False; don't exit or raise exception
  59. from ....exception import TransactionChainMismatch
  60. try:
  61. self.check_correct_chain()
  62. except TransactionChainMismatch:
  63. return False
  64. o = self.txobj
  65. def do_mismatch_err(io, j, k, desc):
  66. m = 'A compromised online installation may have altered your serialized data!'
  67. fs = '\n{} mismatch!\n{}\n orig: {}\n serialized: {}'
  68. die(3, fs.format(desc.upper(), m, getattr(io[0], k), o[j]))
  69. if o['from'] != self.inputs[0].addr:
  70. do_mismatch_err(self.inputs, 'from', 'addr', 'from-address')
  71. if self.outputs:
  72. if o['to'] != self.outputs[0].addr:
  73. do_mismatch_err(self.outputs, 'to', 'addr', 'to-address')
  74. if o['amt'] != self.outputs[0].amt:
  75. do_mismatch_err(self.outputs, 'amt', 'amt', 'amount')
  76. msg_r(f'Signing transaction{tx_num_str}...')
  77. try:
  78. await self.do_sign(o, keys[0].sec.wif)
  79. msg('OK')
  80. from ....tx import SignedTX
  81. return await SignedTX(cfg=self.cfg, data=self.__dict__, automount=self.automount)
  82. except Exception as e:
  83. msg(f'{e}: transaction signing failed!')
  84. return False
  85. class TokenUnsigned(TokenCompleted, Unsigned):
  86. desc = 'unsigned transaction'
  87. def parse_txfile_serialized_data(self):
  88. d = Unsigned.parse_txfile_serialized_data(self)
  89. o = self.txobj
  90. o['token_addr'] = TokenAddr(self.proto, d['token_addr'])
  91. o['decimals'] = Int(d['decimals'])
  92. t = Token(self.cfg, self.proto, o['token_addr'], o['decimals'])
  93. o['data'] = t.create_data(o['to'], o['amt'])
  94. o['token_to'] = t.transferdata2sendaddr(o['data'])
  95. async def do_sign(self, o, wif):
  96. t = Token(self.cfg, self.proto, o['token_addr'], o['decimals'])
  97. tx_in = t.make_tx_in(
  98. to_addr = o['to'],
  99. amt = o['amt'],
  100. gas = self.gas,
  101. gasPrice = o['gasPrice'],
  102. nonce = o['nonce'])
  103. res = await t.txsign(tx_in, wif, o['from'], chain_id=o['chainId'])
  104. self.serialized = res.txhex
  105. self.coin_txid = res.txid
  106. class AutomountUnsigned(TxBase.AutomountUnsigned, Unsigned):
  107. pass
  108. class TokenAutomountUnsigned(TxBase.AutomountUnsigned, TokenUnsigned):
  109. pass