unsigned.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  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, ContractAddr
  18. from ..contract import Token, THORChainRouterContract
  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': (
  31. self.proto.coin_amt(d['startGas']).toWei() if '.' in d['startGas'] # for backward compat
  32. else int(d['startGas'])),
  33. 'nonce': ETHNonce(d['nonce']),
  34. 'chainId': None if d['chainId'] == 'None' else Int(d['chainId']),
  35. 'data': HexStr(d['data'])}
  36. self.gas = o['startGas']
  37. self.txobj = o
  38. return d # 'token_addr', 'decimals' required by Token subclass
  39. async def do_sign(self, o, wif):
  40. o_conv = {
  41. 'to': bytes.fromhex(o['to'] or ''),
  42. 'startgas': o['startGas'],
  43. 'gasprice': o['gasPrice'].toWei(),
  44. 'value': o['amt'].toWei() if o['amt'] else 0,
  45. 'nonce': o['nonce'],
  46. 'data': self.swap_memo.encode() if self.is_swap else bytes.fromhex(o['data'])}
  47. from ..pyethereum.transactions import Transaction
  48. etx = Transaction(**o_conv).sign(wif, o['chainId'])
  49. assert etx.sender.hex() == o['from'], (
  50. 'Sender address recovered from signature does not match true sender')
  51. from .. import rlp
  52. self.serialized = rlp.encode(etx).hex()
  53. self.coin_txid = CoinTxID(etx.hash.hex())
  54. if o['data']: # contract-creating transaction
  55. if o['to']:
  56. if not self.is_swap:
  57. raise ValueError('contract-creating transaction cannot have to-address')
  58. else:
  59. self.txobj['token_addr'] = ContractAddr(self.proto, etx.creates.hex())
  60. async def sign(self, tx_num_str, keys): # return TX object or False; don't exit or raise exception
  61. from ....exception import TransactionChainMismatch
  62. try:
  63. self.check_correct_chain()
  64. except TransactionChainMismatch:
  65. return False
  66. o = self.txobj
  67. def do_mismatch_err(io, j, k, desc):
  68. m = 'A compromised online installation may have altered your serialized data!'
  69. fs = '\n{} mismatch!\n{}\n orig: {}\n serialized: {}'
  70. die(3, fs.format(desc.upper(), m, getattr(io[0], k), o[j]))
  71. if o['from'] != self.inputs[0].addr:
  72. do_mismatch_err(self.inputs, 'from', 'addr', 'from-address')
  73. if self.outputs:
  74. if o['to'] != self.outputs[0].addr:
  75. do_mismatch_err(self.outputs, 'to', 'addr', 'to-address')
  76. if o['amt'] != self.outputs[0].amt:
  77. do_mismatch_err(self.outputs, 'amt', 'amt', 'amount')
  78. msg_r(f'Signing transaction{tx_num_str}...')
  79. try:
  80. await self.do_sign(o, keys[0].sec.wif)
  81. msg('OK')
  82. from ....tx import SignedTX
  83. tx = await SignedTX(cfg=self.cfg, data=self.__dict__, automount=self.automount)
  84. tx.check_serialized_integrity()
  85. return tx
  86. except Exception as e:
  87. msg(f'{e}: transaction signing failed!')
  88. return False
  89. class TokenUnsigned(TokenCompleted, Unsigned):
  90. desc = 'unsigned transaction'
  91. def parse_txfile_serialized_data(self):
  92. d = Unsigned.parse_txfile_serialized_data(self)
  93. o = self.txobj
  94. o['token_addr'] = ContractAddr(self.proto, d['token_addr'])
  95. o['decimals'] = Int(d['decimals'])
  96. o['token_to'] = o['to']
  97. if self.is_swap:
  98. o['expiry'] = Int(d['expiry'])
  99. async def do_sign(self, o, wif):
  100. t = Token(self.cfg, self.proto, o['token_addr'], decimals=o['decimals'])
  101. tdata = t.create_transfer_data(o['to'], o['amt'], op=self.token_op)
  102. tx_in = t.make_tx_in(gas=self.gas, gasPrice=o['gasPrice'], nonce=o['nonce'], data=tdata)
  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. if self.is_swap:
  107. c = THORChainRouterContract(self.cfg, self.proto, o['to'], decimals=o['decimals'])
  108. cdata = c.create_deposit_with_expiry_data(
  109. self.token_vault_addr,
  110. o['token_addr'],
  111. o['amt'],
  112. self.swap_memo.encode(),
  113. o['expiry'])
  114. tx_in = c.make_tx_in(
  115. gas = self.gas * (7 if self.cfg.test_suite else 2),
  116. gasPrice = o['gasPrice'],
  117. nonce = o['nonce'] + 1,
  118. data = cdata)
  119. res = await t.txsign(tx_in, wif, o['from'], chain_id=o['chainId'])
  120. self.serialized2 = res.txhex
  121. self.coin_txid2 = res.txid
  122. class AutomountUnsigned(TxBase.AutomountUnsigned, Unsigned):
  123. pass
  124. class TokenAutomountUnsigned(TxBase.AutomountUnsigned, TokenUnsigned):
  125. pass