xmrwallet.py 61 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. xmrwallet.py - MoneroWalletOps class
  20. """
  21. import re,time,json,atexit
  22. from collections import namedtuple
  23. from pathlib import Path
  24. from .objmethods import MMGenObject,HiliteStr,InitErrors
  25. from .obj import CoinTxID,Int
  26. from .color import red,yellow,green,blue,cyan,pink,orange,purple,gray
  27. from .util import (
  28. msg,
  29. msg_r,
  30. gmsg,
  31. bmsg,
  32. ymsg,
  33. rmsg,
  34. gmsg_r,
  35. pp_msg,
  36. die,
  37. fmt,
  38. suf,
  39. async_run,
  40. make_timestr,
  41. make_chksum_N,
  42. capfirst,
  43. list_gen,
  44. fmt_dict
  45. )
  46. from .fileutil import get_data_from_file
  47. from .seed import SeedID
  48. from .protocol import init_proto
  49. from .proto.btc.common import b58a
  50. from .addr import CoinAddr,AddrIdx
  51. from .addrlist import KeyAddrList,ViewKeyAddrList,AddrIdxList
  52. from .rpc import json_encoder
  53. from .proto.xmr.rpc import MoneroRPCClient,MoneroWalletRPCClient
  54. from .proto.xmr.daemon import MoneroWalletDaemon
  55. from .ui import keypress_confirm
  56. from .autosign import Autosign
  57. xmrwallet_uargs = namedtuple('xmrwallet_uargs',[
  58. 'infile',
  59. 'wallets',
  60. 'spec',
  61. ])
  62. xmrwallet_uarg_info = (
  63. lambda e,hp: {
  64. 'daemon': e('HOST:PORT', hp),
  65. 'tx_relay_daemon': e('HOST:PORT[:PROXY_IP:PROXY_PORT]', rf'({hp})(?::({hp}))?'),
  66. 'newaddr_spec': e('WALLET_NUM[:ACCOUNT][,"label text"]', r'(\d+)(?::(\d+))?(?:,(.*))?'),
  67. 'transfer_spec': e('SOURCE_WALLET_NUM:ACCOUNT:ADDRESS,AMOUNT', rf'(\d+):(\d+):([{b58a}]+),([0-9.]+)'),
  68. 'sweep_spec': e('SOURCE_WALLET_NUM:ACCOUNT[,DEST_WALLET_NUM]', r'(\d+):(\d+)(?:,(\d+))?'),
  69. 'label_spec': e('WALLET_NUM:ACCOUNT:ADDRESS,"label text"', r'(\d+):(\d+):(\d+),(.*)'),
  70. })(
  71. namedtuple('uarg_info_entry',['annot','pat']),
  72. r'(?:[^:]+):(?:\d+)'
  73. )
  74. def get_autosign_obj(cfg):
  75. from .cfg import Config
  76. return Autosign(
  77. Config({
  78. 'mountpoint': cfg.autosign_mountpoint,
  79. 'test_suite': cfg.test_suite,
  80. 'test_suite_root_pfx': cfg.test_suite_root_pfx,
  81. 'coins': 'xmr',
  82. 'online': not cfg.offline,
  83. })
  84. )
  85. # required to squelch pylint:
  86. def fmt_amt(amt):
  87. return str(amt)
  88. def hl_amt(amt):
  89. return str(amt)
  90. tx_priorities = {
  91. 1: 'low',
  92. 2: 'normal',
  93. 3: 'high',
  94. 4: 'highest'
  95. }
  96. class XMRWalletAddrSpec(HiliteStr,InitErrors,MMGenObject):
  97. color = 'cyan'
  98. width = 0
  99. trunc_ok = False
  100. min_len = 5 # 1:0:0
  101. max_len = 14 # 9999:9999:9999
  102. def __new__(cls,arg1,arg2=None,arg3=None):
  103. if isinstance(arg1,cls):
  104. return arg1
  105. try:
  106. if isinstance(arg1,str):
  107. me = str.__new__(cls,arg1)
  108. m = re.fullmatch( '({n}):({n}):({n}|None)'.format(n=r'[0-9]{1,4}'), arg1 )
  109. assert m is not None, f'{arg1!r}: invalid XMRWalletAddrSpec'
  110. for e in m.groups():
  111. if len(e) != 1 and e[0] == '0':
  112. die(2,f'{e}: leading zeroes not permitted in XMRWalletAddrSpec element')
  113. me.wallet = AddrIdx(m[1])
  114. me.account = int(m[2])
  115. me.account_address = None if m[3] == 'None' else int(m[3])
  116. else:
  117. me = str.__new__(cls,f'{arg1}:{arg2}:{arg3}')
  118. for arg in [arg1,arg2] + ([] if arg3 is None else [arg3]):
  119. assert isinstance(arg,int), f'{arg}: XMRWalletAddrSpec component not of type int'
  120. assert arg is None or arg <= 9999, f'{arg}: XMRWalletAddrSpec component greater than 9999'
  121. me.wallet = AddrIdx(arg1)
  122. me.account = arg2
  123. me.account_address = arg3
  124. return me
  125. except Exception as e:
  126. return cls.init_fail(e,me)
  127. def is_xmr_tx_file(cfg,fn):
  128. try:
  129. MoneroMMGenTX.Completed(cfg,fn)
  130. return True
  131. except Exception as e:
  132. if not 'MoneroMMGenTXFileParseError' in type(e).__name__:
  133. ymsg(f'\n{type(e).__name__}: {e}')
  134. return False
  135. class MoneroMMGenFile:
  136. silent_load = False
  137. def make_chksum(self,keys=None):
  138. res = json.dumps(
  139. dict( (k,v) for k,v in self.data._asdict().items() if (not keys or k in keys) ),
  140. cls = json_encoder
  141. )
  142. return make_chksum_N( res, rounds=1, nchars=self.chksum_nchars, upper=False )
  143. @property
  144. def base_chksum(self):
  145. return self.make_chksum(self.base_chksum_fields)
  146. @property
  147. def full_chksum(self):
  148. return self.make_chksum(self.full_chksum_fields) if self.full_chksum_fields else None
  149. def check_checksums(self,d_wrap):
  150. for k in ('base_chksum','full_chksum'):
  151. a = getattr(self,k)
  152. if a is not None:
  153. b = d_wrap[k]
  154. assert a == b, f'{k} mismatch: {a} != {b}'
  155. def make_wrapped_data(self,in_data):
  156. out = {
  157. 'base_chksum': self.base_chksum,
  158. 'full_chksum': self.full_chksum,
  159. 'data': in_data,
  160. } if self.full_chksum else {
  161. 'base_chksum': self.base_chksum,
  162. 'data': in_data,
  163. }
  164. return json.dumps(
  165. { self.data_label: out },
  166. cls = json_encoder,
  167. indent = 2,
  168. )
  169. def extract_data_from_file(self,cfg,fn):
  170. return json.loads(
  171. get_data_from_file( cfg, str(fn), self.desc, silent=self.silent_load )
  172. )[self.data_label]
  173. class MoneroMMGenTX:
  174. class Base(MoneroMMGenFile):
  175. data_label = 'MoneroMMGenTX'
  176. # both base_chksum and full_chksum are used to make the filename stem, so we must not include
  177. # fields that change when TX is signed and submitted (e.g. ‘sign_time’, ‘submit_time’)
  178. base_chksum_fields = {
  179. 'op',
  180. 'create_time',
  181. 'network',
  182. 'seed_id',
  183. 'source',
  184. 'dest',
  185. 'amount' }
  186. full_chksum_fields = {
  187. 'op',
  188. 'create_time',
  189. 'network',
  190. 'seed_id',
  191. 'source',
  192. 'dest',
  193. 'amount',
  194. 'fee',
  195. 'blob' }
  196. oneline_fs = '{a:7} {b:8} {c:19} {d:13} {e:8} {f:6} {x:2} {g:6} {h:17} {j}'
  197. chksum_nchars = 6
  198. xmrwallet_tx_data = namedtuple('xmrwallet_tx_data',[
  199. 'op',
  200. 'create_time',
  201. 'sign_time',
  202. 'submit_time',
  203. 'network',
  204. 'seed_id',
  205. 'source',
  206. 'dest',
  207. 'dest_address',
  208. 'txid',
  209. 'amount',
  210. 'priority',
  211. 'fee',
  212. 'blob',
  213. 'metadata',
  214. 'unsigned_txset',
  215. 'signed_txset',
  216. 'complete',
  217. ])
  218. def __init__(self):
  219. self.name = type(self).__name__
  220. @property
  221. def src_wallet_idx(self):
  222. return int(self.data.source.split(':')[0])
  223. def get_info_oneline(self,indent='',cols=None):
  224. d = self.data
  225. return self.oneline_fs.format(
  226. a = yellow(d.network),
  227. b = d.seed_id.hl(),
  228. c = make_timestr(d.submit_time if d.submit_time is not None else d.create_time),
  229. d = orange(self.file_id),
  230. e = purple(capfirst(d.op.ljust(8))),
  231. f = red('{}:{}'.format(d.source.wallet,d.source.account).ljust(6)),
  232. g = red('{}:{}'.format(d.dest.wallet,d.dest.account).ljust(6)) if d.dest else cyan('ext '),
  233. h = d.amount.fmt( color=True, iwidth=4, prec=12 ),
  234. j = d.dest_address.fmt(width=cols-95,color=True) if cols else d.dest_address.hl(),
  235. x = '->'
  236. )
  237. def get_info(self,indent='',cols=None):
  238. d = self.data
  239. pmt_id = d.dest_address.parsed.payment_id
  240. fs = '\n'.join(list_gen(
  241. ['Info for transaction {a} [Seed ID: {b}. Network: {c}]:'],
  242. [' TxID: {d}'],
  243. [' Created: {e:19} [{f}]'],
  244. [' Signed: {g:19} [{h}]', d.sign_time],
  245. [' Submitted: {s:19} [{t}]', d.submit_time],
  246. [' Type: {i}{S}'],
  247. [' From: Wallet {j}, account {k}'],
  248. [' To: Wallet {x}, account {y}, address {z}', d.dest],
  249. [' Amount: {m} XMR'],
  250. [' Priority: {F}', d.priority],
  251. [' Fee: {n} XMR'],
  252. [' Dest: {o}'],
  253. [' Payment ID: {P}', pmt_id],
  254. ))
  255. from .util2 import format_elapsed_hr
  256. return fmt(fs,strip_char='\t',indent=indent).format(
  257. a = orange(self.file_id),
  258. b = d.seed_id.hl(),
  259. c = yellow(d.network.upper()),
  260. d = d.txid.hl(),
  261. e = make_timestr(d.create_time),
  262. f = format_elapsed_hr(d.create_time),
  263. g = make_timestr(d.sign_time) if d.sign_time else None,
  264. h = format_elapsed_hr(d.sign_time) if d.sign_time else None,
  265. i = blue(capfirst(d.op)),
  266. j = d.source.wallet.hl(),
  267. k = red(f'#{d.source.account}'),
  268. m = d.amount.hl(),
  269. F = (Int(d.priority).hl() + f' [{tx_priorities[d.priority]}]') if d.priority else None,
  270. n = d.fee.hl(),
  271. o = d.dest_address.hl(),
  272. P = pink(pmt_id.hex()) if pmt_id else None,
  273. s = make_timestr(d.submit_time) if d.submit_time else None,
  274. S = pink(f" [cold signed{', submitted' if d.complete else ''}]") if d.signed_txset else '',
  275. t = format_elapsed_hr(d.submit_time) if d.submit_time else None,
  276. x = d.dest.wallet.hl() if d.dest else None,
  277. y = red(f'#{d.dest.account}') if d.dest else None,
  278. z = red(f'#{d.dest.account_address}') if d.dest else None,
  279. )
  280. @property
  281. def file_id(self):
  282. return (self.base_chksum + ('-' + self.full_chksum if self.full_chksum else '')).upper()
  283. def write(self,delete_metadata=False,ask_write=True,ask_overwrite=True):
  284. dict_data = self.data._asdict()
  285. if delete_metadata:
  286. dict_data['metadata'] = None
  287. fn = '{a}-XMR[{b!s}]{c}.{d}'.format(
  288. a = self.file_id,
  289. b = self.data.amount,
  290. c = '' if self.data.network == 'mainnet' else f'.{self.data.network}',
  291. d = self.ext
  292. )
  293. if self.cfg.autosign:
  294. fn = get_autosign_obj(self.cfg).xmr_tx_dir / fn
  295. from .fileutil import write_data_to_file
  296. write_data_to_file(
  297. cfg = self.cfg,
  298. outfile = str(fn),
  299. data = self.make_wrapped_data(dict_data),
  300. desc = self.desc,
  301. ask_write = ask_write,
  302. ask_write_default_yes = not ask_write,
  303. ask_overwrite = ask_overwrite,
  304. ignore_opt_outdir = self.cfg.autosign )
  305. class New(Base):
  306. def __init__(self,*args,**kwargs):
  307. super().__init__()
  308. assert not args, 'Non-keyword args not permitted'
  309. if '_in_tx' in kwargs:
  310. in_data = kwargs.pop('_in_tx').data._asdict()
  311. in_data.update(kwargs)
  312. else:
  313. in_data = kwargs
  314. d = namedtuple('monero_tx_in_data_tuple',in_data)(**in_data)
  315. self.cfg = d.cfg
  316. proto = init_proto( self.cfg, 'xmr', network=d.network, need_amt=True )
  317. now = int(time.time())
  318. self.data = self.xmrwallet_tx_data(
  319. op = d.op,
  320. create_time = now if self.name in ('NewSigned','NewUnsigned') else getattr(d,'create_time',None),
  321. sign_time = now if self.name in ('NewSigned','NewColdSigned') else getattr(d,'sign_time',None),
  322. submit_time = now if self.name == 'NewSubmitted' else None,
  323. network = d.network,
  324. seed_id = SeedID(sid=d.seed_id),
  325. source = XMRWalletAddrSpec(d.source),
  326. dest = None if d.dest is None else XMRWalletAddrSpec(d.dest),
  327. dest_address = CoinAddr(proto,d.dest_address),
  328. txid = CoinTxID(d.txid),
  329. amount = proto.coin_amt(d.amount,from_unit='atomic'),
  330. priority = self.cfg.priority if self.name in ('NewSigned','NewUnsigned') else d.priority,
  331. fee = proto.coin_amt(d.fee,from_unit='atomic'),
  332. blob = d.blob,
  333. metadata = d.metadata,
  334. unsigned_txset = d.unsigned_txset,
  335. signed_txset = getattr(d,'signed_txset',None),
  336. complete = self.name in ('NewSigned','NewSubmitted'),
  337. )
  338. class NewUnsigned(New):
  339. desc = 'unsigned transaction'
  340. ext = 'rawtx'
  341. signed = False
  342. class NewSigned(New):
  343. desc = 'signed transaction'
  344. ext = 'sigtx'
  345. signed = True
  346. class NewColdSigned(NewSigned):
  347. pass
  348. class NewSubmitted(NewColdSigned):
  349. desc = 'submitted transaction'
  350. ext = 'subtx'
  351. class Completed(Base):
  352. desc = 'transaction'
  353. forbidden_fields = ()
  354. def __init__(self,cfg,fn):
  355. super().__init__()
  356. self.cfg = cfg
  357. self.fn = Path(fn)
  358. try:
  359. d_wrap = self.extract_data_from_file( cfg, fn )
  360. except Exception as e:
  361. die( 'MoneroMMGenTXFileParseError', f'{type(e).__name__}: {e}\nCould not load transaction file' )
  362. if 'unsigned_txset' in d_wrap['data']: # post-autosign
  363. self.full_chksum_fields &= set(d_wrap['data']) # allow for added chksum fields in future
  364. else:
  365. self.full_chksum_fields = set(d_wrap['data']) - {'metadata'}
  366. for key in self.xmrwallet_tx_data._fields: # backwards compat: fill in missing fields
  367. if not key in d_wrap['data']:
  368. d_wrap['data'][key] = None
  369. d = self.xmrwallet_tx_data(**d_wrap['data'])
  370. if self.name not in ('View','Completed'):
  371. assert fn.name.endswith('.'+self.ext), 'TX file {fn} has incorrect extension (not {self.ext!r})'
  372. assert getattr(d,self.req_field), f'{self.name} TX missing required field {self.req_field!r}'
  373. assert bool(d.sign_time) == self.signed, '{a} has {b}sign time!'.format(
  374. a = self.desc,
  375. b = 'no ' if self.signed else'' )
  376. for f in self.forbidden_fields:
  377. assert not getattr(d,f), f'{self.name} TX mismatch: contains forbidden field {f!r}'
  378. proto = init_proto( cfg, 'xmr', network=d.network, need_amt=True )
  379. self.data = self.xmrwallet_tx_data(
  380. op = d.op,
  381. create_time = d.create_time,
  382. sign_time = d.sign_time,
  383. submit_time = d.submit_time,
  384. network = d.network,
  385. seed_id = SeedID(sid=d.seed_id),
  386. source = XMRWalletAddrSpec(d.source),
  387. dest = None if d.dest is None else XMRWalletAddrSpec(d.dest),
  388. dest_address = CoinAddr(proto,d.dest_address),
  389. txid = CoinTxID(d.txid),
  390. amount = proto.coin_amt(d.amount),
  391. priority = d.priority,
  392. fee = proto.coin_amt(d.fee),
  393. blob = d.blob,
  394. metadata = d.metadata,
  395. unsigned_txset = d.unsigned_txset,
  396. signed_txset = d.signed_txset,
  397. complete = d.complete,
  398. )
  399. self.check_checksums(d_wrap)
  400. class Unsigned(Completed):
  401. desc = 'unsigned transaction'
  402. ext = 'rawtx'
  403. signed = False
  404. req_field = 'unsigned_txset'
  405. forbidden_fields = ('signed_txset',)
  406. class Signed(Completed):
  407. desc = 'signed transaction'
  408. ext = 'sigtx'
  409. signed = True
  410. req_field = 'blob'
  411. forbidden_fields = ('signed_txset','unsigned_txset')
  412. class ColdSigned(Signed):
  413. req_field = 'signed_txset'
  414. forbidden_fields = ()
  415. class Submitted(ColdSigned):
  416. desc = 'submitted transaction'
  417. ext = 'subtx'
  418. silent_load = True
  419. class View(Completed):
  420. silent_load = True
  421. class MoneroWalletOutputsFile:
  422. class Base(MoneroMMGenFile):
  423. desc = 'wallet outputs'
  424. data_label = 'MoneroMMGenWalletOutputsFile'
  425. base_chksum_fields = {'seed_id','wallet_index','outputs_data_hex',}
  426. full_chksum_fields = {'seed_id','wallet_index','outputs_data_hex','signed_key_images'}
  427. fn_fs = '{a}-outputs-{b}.{c}'
  428. ext_offset = 25 # len('-outputs-') + len(chksum) ({b})
  429. chksum_nchars = 16
  430. data_tuple = namedtuple('wallet_outputs_data',[
  431. 'seed_id',
  432. 'wallet_index',
  433. 'outputs_data_hex',
  434. 'signed_key_images',
  435. 'sign',
  436. 'imported',
  437. ])
  438. def __init__(self,cfg):
  439. self.name = type(self).__name__
  440. self.cfg = cfg
  441. def write(self,add_suf=''):
  442. from .fileutil import write_data_to_file
  443. write_data_to_file(
  444. cfg = self.cfg,
  445. outfile = str(self.get_outfile( self.cfg, self.wallet_fn )) + add_suf,
  446. data = self.make_wrapped_data(self.data._asdict()),
  447. desc = self.desc,
  448. ask_overwrite = False,
  449. ignore_opt_outdir = True )
  450. def get_outfile(self,cfg,wallet_fn):
  451. return (
  452. get_autosign_obj(cfg).xmr_outputs_dir if cfg.autosign else
  453. wallet_fn.parent ) / self.fn_fs.format(
  454. a = wallet_fn.name,
  455. b = self.base_chksum,
  456. c = self.ext,
  457. )
  458. def get_wallet_fn(self,fn):
  459. assert fn.name.endswith(f'.{self.ext}'), (
  460. f'{self.name}: filename does not end with {"."+self.ext!r}'
  461. )
  462. return fn.parent / fn.name[:-(len(self.ext)+self.ext_offset+1)]
  463. def get_info(self,indent=''):
  464. if self.data.signed_key_images is not None:
  465. data = self.data.signed_key_images or []
  466. return f'{indent}{self.wallet_fn.name}: {len(data)} signed key image{suf(data)}'
  467. else:
  468. return f'{indent}{self.wallet_fn.name}: no key images'
  469. class New(Base):
  470. ext = 'raw'
  471. def __init__(self, parent, wallet_fn, data, wallet_idx=None, sign=False):
  472. super().__init__(parent.cfg)
  473. self.wallet_fn = wallet_fn
  474. init_data = dict.fromkeys(self.data_tuple._fields)
  475. init_data.update({
  476. 'seed_id': parent.kal.al_id.sid,
  477. 'wallet_index': wallet_idx or parent.get_idx_from_fn(wallet_fn),
  478. })
  479. if sign:
  480. init_data['sign'] = sign
  481. init_data.update({k:v for k,v in data.items() if k in init_data})
  482. self.data = self.data_tuple(**init_data)
  483. class Completed(New):
  484. def __init__( self, parent, fn=None, wallet_fn=None ):
  485. def check_equal(desc,a,b):
  486. assert a == b, f'{desc} mismatch: {a} (from file) != {b} (from filename)'
  487. fn = fn or self.get_outfile( parent.cfg, wallet_fn )
  488. wallet_fn = wallet_fn or self.get_wallet_fn(fn)
  489. d_wrap = self.extract_data_from_file( parent.cfg, fn )
  490. data = d_wrap['data']
  491. check_equal( 'Seed ID', data['seed_id'], parent.kal.al_id.sid )
  492. wallet_idx = parent.get_idx_from_fn(wallet_fn)
  493. check_equal( 'Wallet index', data['wallet_index'], wallet_idx )
  494. super().__init__(
  495. parent = parent,
  496. wallet_fn = wallet_fn,
  497. data = data,
  498. wallet_idx = wallet_idx,
  499. )
  500. self.check_checksums(d_wrap)
  501. @classmethod
  502. def find_fn_from_wallet_fn(cls,cfg,wallet_fn,ret_on_no_match=False):
  503. path = get_autosign_obj(cfg).xmr_outputs_dir or Path()
  504. pat = cls.fn_fs.format(
  505. a = wallet_fn.name,
  506. b = f'[0-9a-f]{{{cls.chksum_nchars}}}\\',
  507. c = cls.ext,
  508. )
  509. matches = [f for f in path.iterdir() if re.match(pat,f.name)]
  510. if not matches and ret_on_no_match:
  511. return None
  512. if not matches or len(matches) > 1:
  513. die(2,"{a} matching pattern {b!r} found in '{c}'!".format(
  514. a = 'No files' if not matches else 'More than one file',
  515. b = pat,
  516. c = path
  517. ))
  518. return matches[0]
  519. class Unsigned(Completed):
  520. pass
  521. class SignedNew(New):
  522. desc = 'signed key images'
  523. ext = 'sig'
  524. class Signed(Completed,SignedNew):
  525. pass
  526. class MoneroWalletDumpFile:
  527. class Base:
  528. desc = 'Monero wallet dump'
  529. data_label = 'MoneroMMGenWalletDumpFile'
  530. base_chksum_fields = {'seed_id','wallet_index','wallet_metadata'}
  531. full_chksum_fields = None
  532. ext = 'dump'
  533. ext_offset = 0
  534. data_tuple = namedtuple('wallet_dump_data',[
  535. 'seed_id',
  536. 'wallet_index',
  537. 'wallet_metadata',
  538. ])
  539. def get_outfile(self,cfg,wallet_fn):
  540. return wallet_fn.parent / f'{wallet_fn.name}.{self.ext}'
  541. class New(Base,MoneroWalletOutputsFile.New):
  542. pass
  543. class Completed(Base,MoneroWalletOutputsFile.Completed):
  544. pass
  545. class MoneroWalletOps:
  546. ops = (
  547. 'create',
  548. 'create_offline',
  549. 'sync',
  550. 'list',
  551. 'view',
  552. 'listview',
  553. 'new',
  554. 'transfer',
  555. 'sweep',
  556. 'relay',
  557. 'txview',
  558. 'txlist',
  559. 'label',
  560. 'sign',
  561. 'submit',
  562. 'resubmit',
  563. 'abort',
  564. 'dump',
  565. 'restore',
  566. 'export_outputs',
  567. 'import_key_images' )
  568. kafile_arg_ops = (
  569. 'create',
  570. 'sync',
  571. 'list',
  572. 'view',
  573. 'listview',
  574. 'label',
  575. 'new',
  576. 'transfer',
  577. 'sweep',
  578. 'dump',
  579. 'restore' )
  580. opts = (
  581. 'wallet_dir',
  582. 'daemon',
  583. 'tx_relay_daemon',
  584. 'use_internal_keccak_module',
  585. 'hash_preset',
  586. 'restore_height',
  587. 'no_start_wallet_daemon',
  588. 'no_stop_wallet_daemon',
  589. 'no_relay',
  590. 'watch_only',
  591. 'autosign' )
  592. pat_opts = ('daemon','tx_relay_daemon')
  593. class base(MMGenObject):
  594. opts = ('wallet_dir',)
  595. trust_monerod = False
  596. do_umount = True
  597. def __init__(self,cfg,uarg_tuple):
  598. def gen_classes():
  599. for cls in type(self).__mro__:
  600. yield cls
  601. if cls.__name__ == 'base':
  602. break
  603. self.name = type(self).__name__
  604. self.cfg = cfg
  605. classes = tuple(gen_classes())
  606. self.opts = tuple(set(opt for cls in classes for opt in cls.opts))
  607. if not hasattr(self,'stem'):
  608. self.stem = self.name
  609. global uarg, uarg_info, fmt_amt, hl_amt
  610. uarg = uarg_tuple
  611. uarg_info = xmrwallet_uarg_info
  612. def fmt_amt(amt):
  613. return self.proto.coin_amt(amt,from_unit='atomic').fmt( iwidth=5, prec=12, color=True )
  614. def hl_amt(amt):
  615. return self.proto.coin_amt(amt,from_unit='atomic').hl()
  616. id_cur = None
  617. for cls in classes:
  618. if id(cls.check_uopts) != id_cur:
  619. cls.check_uopts(self)
  620. id_cur = id(cls.check_uopts)
  621. self.proto = init_proto( cfg, 'xmr', network=self.cfg.network, need_amt=True )
  622. if cfg.autosign:
  623. self.asi = get_autosign_obj(cfg)
  624. self.pre_init_action()
  625. def check_uopts(self):
  626. def check_pat_opt(name):
  627. val = getattr(self.cfg,name)
  628. if not re.fullmatch( uarg_info[name].pat, val, re.ASCII ):
  629. die(1,'{!r}: invalid value for --{}: it must have format {!r}'.format(
  630. val,
  631. name.replace('_','-'),
  632. uarg_info[name].annot
  633. ))
  634. for attr in self.cfg.__dict__:
  635. if attr in MoneroWalletOps.opts and not attr in self.opts:
  636. die(1,'Option --{} not supported for {!r} operation'.format(
  637. attr.replace('_','-'),
  638. self.name,
  639. ))
  640. for opt in MoneroWalletOps.pat_opts:
  641. if getattr(self.cfg,opt,None):
  642. check_pat_opt(opt)
  643. def parse_tx_relay_opt(self):
  644. return re.fullmatch(
  645. uarg_info['tx_relay_daemon'].pat,
  646. self.cfg.tx_relay_daemon,
  647. re.ASCII )
  648. def display_tx_relay_info(self,indent=''):
  649. m = self.parse_tx_relay_opt()
  650. msg(fmt(f"""
  651. TX relay info:
  652. Host: {blue(m[1])}
  653. Proxy: {blue(m[2] or 'None')}
  654. """,strip_char='\t',indent=indent))
  655. def pre_init_action(self):
  656. pass
  657. def post_main_success(self):
  658. pass
  659. def post_main_failure(self):
  660. pass
  661. async def stop_wallet_daemon(self):
  662. pass
  663. def post_mount_action(self):
  664. pass
  665. def mount_removable_device(self):
  666. if self.cfg.autosign:
  667. if not self.asi.get_insert_status():
  668. die(1,'Removable device not present!')
  669. if self.do_umount:
  670. import atexit
  671. atexit.register(lambda: self.asi.do_umount())
  672. self.asi.do_mount()
  673. self.post_mount_action()
  674. class wallet(base):
  675. opts = (
  676. 'use_internal_keccak_module',
  677. 'hash_preset',
  678. 'daemon',
  679. 'no_start_wallet_daemon',
  680. 'no_stop_wallet_daemon',
  681. 'autosign',
  682. 'watch_only',
  683. )
  684. wallet_offline = False
  685. wallet_exists = True
  686. start_daemon = True
  687. skip_wallet_check = False # for debugging
  688. def __init__(self,cfg,uarg_tuple):
  689. def wallet_exists(fn):
  690. try:
  691. fn.stat()
  692. except:
  693. return False
  694. else:
  695. return True
  696. def check_wallets():
  697. for d in self.addr_data:
  698. fn = self.get_wallet_fn(d)
  699. exists = wallet_exists(fn)
  700. if exists and not self.wallet_exists:
  701. die(1,f"Wallet '{fn}' already exists!")
  702. elif not exists and self.wallet_exists:
  703. die(1,f"Wallet '{fn}' not found!")
  704. super().__init__(cfg,uarg_tuple)
  705. if self.cfg.offline or (self.name == 'create' and self.cfg.restore_height is None):
  706. self.wallet_offline = True
  707. self.wd = MoneroWalletDaemon(
  708. cfg = self.cfg,
  709. proto = self.proto,
  710. wallet_dir = self.cfg.wallet_dir or '.',
  711. test_suite = self.cfg.test_suite,
  712. monerod_addr = self.cfg.daemon or None,
  713. trust_monerod = self.trust_monerod,
  714. test_monerod = not self.wallet_offline,
  715. )
  716. if self.wallet_offline:
  717. self.wd.usr_daemon_args = ['--offline']
  718. self.c = MoneroWalletRPCClient(
  719. cfg = self.cfg,
  720. daemon = self.wd,
  721. test_connection = False,
  722. )
  723. if self.cfg.offline:
  724. from .wallet import Wallet
  725. self.seed_src = Wallet(
  726. cfg = cfg,
  727. fn = uarg.infile,
  728. ignore_in_fmt = True )
  729. gmsg('\nCreating ephemeral key-address list for offline wallets')
  730. self.kal = KeyAddrList(
  731. cfg = cfg,
  732. proto = self.proto,
  733. seed = self.seed_src.seed,
  734. addr_idxs = uarg.wallets,
  735. skip_chksum_msg = True )
  736. else:
  737. self.mount_removable_device()
  738. # with watch_only, make a second attempt to open the file as KeyAddrList:
  739. for first_try in (True,False):
  740. try:
  741. self.kal = (ViewKeyAddrList if (self.cfg.watch_only and first_try) else KeyAddrList)(
  742. cfg = cfg,
  743. proto = self.proto,
  744. addrfile = str(self.autosign_viewkey_addr_file) if self.cfg.autosign else uarg.infile,
  745. key_address_validity_check = True,
  746. skip_chksum_msg = True )
  747. break
  748. except:
  749. if first_try:
  750. msg(f"Attempting to open '{uarg.infile}' as key-address list")
  751. continue
  752. raise
  753. self.create_addr_data()
  754. if not self.skip_wallet_check:
  755. check_wallets()
  756. if self.start_daemon and not self.cfg.no_start_wallet_daemon:
  757. async_run(self.restart_wallet_daemon())
  758. @classmethod
  759. def get_idx_from_fn(cls,fn):
  760. return int( re.match(r'[0-9a-fA-F]{8}-(\d+)-Monero(WatchOnly)?Wallet.*',fn.name)[1] )
  761. def get_coin_daemon_rpc(self):
  762. host,port = self.cfg.daemon.split(':') if self.cfg.daemon else ('localhost',self.wd.monerod_port)
  763. from .daemon import CoinDaemon
  764. return MoneroRPCClient(
  765. cfg = self.cfg,
  766. proto = self.proto,
  767. daemon = CoinDaemon( self.cfg, 'xmr' ),
  768. host = host,
  769. port = int(port),
  770. user = None,
  771. passwd = None )
  772. @property
  773. def autosign_viewkey_addr_file(self):
  774. from .addrfile import ViewKeyAddrFile
  775. flist = [f for f in self.asi.xmr_dir.iterdir() if f.name.endswith(ViewKeyAddrFile.ext)]
  776. if len(flist) != 1:
  777. die(2,
  778. "{a} viewkey-address files found in autosign mountpoint directory '{b}'!\n".format(
  779. a = 'Multiple' if flist else 'No',
  780. b = self.asi.xmr_dir
  781. )
  782. + 'Have you run ‘mmgen-autosign setup’ on your offline machine with the --xmrwallets option?'
  783. )
  784. else:
  785. return flist[0]
  786. def create_addr_data(self):
  787. if uarg.wallets:
  788. idxs = AddrIdxList(uarg.wallets)
  789. self.addr_data = [d for d in self.kal.data if d.idx in idxs]
  790. if len(self.addr_data) != len(idxs):
  791. die(1,f'List {uarg.wallets!r} contains addresses not present in supplied key-address file')
  792. else:
  793. self.addr_data = self.kal.data
  794. async def restart_wallet_daemon(self):
  795. atexit.register(lambda: async_run(self.stop_wallet_daemon()))
  796. await self.c.restart_daemon()
  797. async def stop_wallet_daemon(self):
  798. if not self.cfg.no_stop_wallet_daemon:
  799. try:
  800. await self.c.stop_daemon()
  801. except KeyboardInterrupt:
  802. ymsg('\nForce killing wallet daemon')
  803. self.c.daemon.force_kill = True
  804. self.c.daemon.stop()
  805. def get_wallet_fn(self,data,watch_only=None):
  806. if watch_only is None:
  807. watch_only = self.cfg.watch_only
  808. return Path(
  809. (self.cfg.wallet_dir or '.'),
  810. '{a}-{b}-Monero{c}Wallet{d}'.format(
  811. a = self.kal.al_id.sid,
  812. b = data.idx,
  813. c = 'WatchOnly' if watch_only else '',
  814. d = f'.{self.cfg.network}' if self.cfg.network != 'mainnet' else '')
  815. )
  816. @property
  817. def add_wallet_desc(self):
  818. return 'offline signing ' if self.cfg.offline else 'watch-only ' if self.cfg.watch_only else ''
  819. async def main(self):
  820. gmsg('\n{a}ing {b} {c}wallet{d}'.format(
  821. a = self.stem.capitalize(),
  822. b = len(self.addr_data),
  823. c = self.add_wallet_desc,
  824. d = suf(self.addr_data) ))
  825. processed = 0
  826. for n,d in enumerate(self.addr_data): # [d.sec,d.addr,d.wallet_passwd,d.viewkey]
  827. fn = self.get_wallet_fn(d)
  828. gmsg('\n{a}ing wallet {b}/{c} ({d})'.format(
  829. a = self.stem.capitalize(),
  830. b = n + 1,
  831. c = len(self.addr_data),
  832. d = fn.name,
  833. ))
  834. processed += await self.process_wallet(
  835. d,
  836. fn,
  837. last = n==len(self.addr_data)-1 )
  838. gmsg(f'\n{processed} wallet{suf(processed)} {self.stem}ed\n')
  839. return processed
  840. def head_msg(self,wallet_idx,fn):
  841. gmsg('\n{a} {b}wallet #{c} ({d})'.format(
  842. a = self.action.capitalize(),
  843. b = self.add_wallet_desc,
  844. c = wallet_idx,
  845. d = fn.name
  846. ))
  847. class rpc:
  848. def __init__(self,parent,d):
  849. self.parent = parent
  850. self.cfg = parent.cfg
  851. self.c = parent.c
  852. self.d = d
  853. self.fn = parent.get_wallet_fn(d)
  854. self.new_tx_cls = (
  855. MoneroMMGenTX.NewUnsigned if self.cfg.watch_only else
  856. MoneroMMGenTX.NewSigned )
  857. def open_wallet(self,desc=None,refresh=True):
  858. add_desc = desc + ' ' if desc else self.parent.add_wallet_desc
  859. gmsg_r(f'\n Opening {add_desc}wallet...')
  860. self.c.call( # returns {}
  861. 'open_wallet',
  862. filename = self.fn.name,
  863. password = self.d.wallet_passwd )
  864. gmsg('done')
  865. if refresh:
  866. gmsg_r(f' Refreshing {add_desc}wallet...')
  867. ret = self.c.call('refresh')
  868. gmsg('done')
  869. if ret['received_money']:
  870. msg(' Wallet has received funds')
  871. def close_wallet(self,desc):
  872. gmsg_r(f'\n Closing {desc} wallet...')
  873. self.c.call('close_wallet')
  874. gmsg_r('done')
  875. async def stop_wallet(self,desc):
  876. msg(f'Stopping {self.c.daemon.desc} on port {self.c.daemon.bind_port}')
  877. gmsg_r(f'\n Stopping {desc} wallet...')
  878. await self.c.stop_daemon(quiet=True) # closes wallet
  879. gmsg_r('done')
  880. def gen_accts_info(self, data, addrs_data, indent=' '):
  881. d = data['subaddress_accounts']
  882. yield indent + f'Accounts of wallet {self.fn.name}:'
  883. fs = indent + ' {a:6} {b:18} {c:<6} {d:%s} {e}' % max(len(e['label']) for e in d)
  884. yield fs.format(a='Index ', b='Base Address', c='nAddrs', d='Label', e='Unlocked Balance')
  885. for i,e in enumerate(d):
  886. yield fs.format(
  887. a = str(e['account_index']),
  888. b = e['base_address'][:15] + '...',
  889. c = len(addrs_data[i]['addresses']),
  890. d = e['label'],
  891. e = fmt_amt(e['unlocked_balance']))
  892. def get_accts(self,print=True):
  893. data = self.c.call('get_accounts')
  894. addrs_data = [
  895. self.c.call('get_address',account_index=i)
  896. for i in range(len(data['subaddress_accounts']))
  897. ]
  898. if print:
  899. msg('\n' + '\n'.join(self.gen_accts_info(data, addrs_data)))
  900. return ( data, addrs_data )
  901. def create_acct(self,label=None):
  902. msg('\n Creating new account...')
  903. ret = self.c.call('create_account', label=label)
  904. msg(' Index: {}'.format( pink(str(ret['account_index'])) ))
  905. msg(' Address: {}'.format( cyan(ret['address']) ))
  906. return (ret['account_index'], ret['address'])
  907. def get_last_acct(self,accts_data):
  908. msg('\n Getting last account...')
  909. ret = accts_data['subaddress_accounts'][-1]
  910. msg(' Index: {}'.format( pink(str(ret['account_index'])) ))
  911. msg(' Address: {}'.format( cyan(ret['base_address']) ))
  912. return (ret['account_index'], ret['base_address'])
  913. def print_addrs(self,accts_data,account):
  914. ret = self.c.call('get_address',account_index=account)
  915. d = ret['addresses']
  916. msg('\n Addresses of account #{} ({}):'.format(
  917. account,
  918. accts_data['subaddress_accounts'][account]['label']))
  919. fs = ' {:6} {:18} {:%s} {}' % max((len(e['label']) for e in d), default=0)
  920. msg(fs.format('Index ','Address','Label','Used'))
  921. for e in d:
  922. msg(fs.format(
  923. str(e['address_index']),
  924. e['address'][:15] + '...',
  925. e['label'],
  926. e['used']
  927. ))
  928. return ret
  929. def create_new_addr(self,account,label=None):
  930. msg_r('\n Creating new address: ')
  931. ret = self.c.call(
  932. 'create_address',
  933. account_index = account,
  934. label = label or f'Sweep from this account [{make_timestr()}]',
  935. )
  936. msg(cyan(ret['address']))
  937. return ret['address']
  938. def get_last_addr(self,account,display=True):
  939. if display:
  940. msg('\n Getting last address:')
  941. ret = self.c.call(
  942. 'get_address',
  943. account_index = account,
  944. )['addresses']
  945. addr = ret[-1]['address']
  946. if display:
  947. msg(' ' + cyan(addr))
  948. return ( addr, len(ret) - 1 )
  949. def set_label(self,account,address_idx,label):
  950. return self.c.call(
  951. 'label_address',
  952. index = { 'major': account, 'minor': address_idx },
  953. label = label
  954. )
  955. def make_transfer_tx(self,account,addr,amt):
  956. res = self.c.call(
  957. 'transfer',
  958. account_index = account,
  959. destinations = [{
  960. 'amount': amt.to_unit('atomic'),
  961. 'address': addr
  962. }],
  963. priority = self.cfg.priority or None,
  964. do_not_relay = True,
  965. get_tx_hex = True,
  966. get_tx_metadata = True
  967. )
  968. return self.new_tx_cls(
  969. cfg = self.cfg,
  970. op = self.parent.name,
  971. network = self.parent.proto.network,
  972. seed_id = self.parent.kal.al_id.sid,
  973. source = XMRWalletAddrSpec(self.parent.source.idx,self.parent.account,None),
  974. dest = None,
  975. dest_address = addr,
  976. txid = res['tx_hash'],
  977. amount = res['amount'],
  978. fee = res['fee'],
  979. blob = res['tx_blob'],
  980. metadata = res['tx_metadata'],
  981. unsigned_txset = res['unsigned_txset'] if self.cfg.watch_only else None,
  982. )
  983. def make_sweep_tx(self,account,dest_acct,dest_addr_idx,addr):
  984. res = self.c.call(
  985. 'sweep_all',
  986. address = addr,
  987. account_index = account,
  988. priority = self.cfg.priority or None,
  989. do_not_relay = True,
  990. get_tx_hex = True,
  991. get_tx_metadata = True
  992. )
  993. if len(res['tx_hash_list']) > 1:
  994. die(3,'More than one TX required. Cannot perform this sweep')
  995. return self.new_tx_cls(
  996. cfg = self.cfg,
  997. op = self.parent.name,
  998. network = self.parent.proto.network,
  999. seed_id = self.parent.kal.al_id.sid,
  1000. source = XMRWalletAddrSpec(self.parent.source.idx,self.parent.account,None),
  1001. dest = XMRWalletAddrSpec(
  1002. (self.parent.dest or self.parent.source).idx,
  1003. dest_acct,
  1004. dest_addr_idx),
  1005. dest_address = addr,
  1006. txid = res['tx_hash_list'][0],
  1007. amount = res['amount_list'][0],
  1008. fee = res['fee_list'][0],
  1009. blob = res['tx_blob_list'][0],
  1010. metadata = res['tx_metadata_list'][0],
  1011. unsigned_txset = res['unsigned_txset'] if self.cfg.watch_only else None,
  1012. )
  1013. def relay_tx(self,tx_hex):
  1014. ret = self.c.call('relay_tx',hex=tx_hex)
  1015. try:
  1016. msg('\n Relayed {}'.format( CoinTxID(ret['tx_hash']).hl() ))
  1017. except:
  1018. msg(f'\n Server returned: {ret!s}')
  1019. class create(wallet):
  1020. stem = 'creat'
  1021. wallet_exists = False
  1022. opts = ('restore_height',)
  1023. def check_uopts(self):
  1024. if self.cfg.restore_height != 'current':
  1025. if int(self.cfg.restore_height or 0) < 0:
  1026. die(1,f'{self.cfg.restore_height}: invalid value for --restore-height (less than zero)')
  1027. async def process_wallet(self,d,fn,last):
  1028. msg_r('') # for pexpect
  1029. if self.cfg.restore_height == 'current':
  1030. restore_height = self.get_coin_daemon_rpc().call_raw('get_height')['height']
  1031. else:
  1032. restore_height = self.cfg.restore_height
  1033. if self.cfg.watch_only:
  1034. ret = self.c.call(
  1035. 'generate_from_keys',
  1036. filename = fn.name,
  1037. password = d.wallet_passwd,
  1038. address = d.addr,
  1039. viewkey = d.viewkey,
  1040. restore_height = restore_height )
  1041. else:
  1042. from .xmrseed import xmrseed
  1043. ret = self.c.call(
  1044. 'restore_deterministic_wallet',
  1045. filename = fn.name,
  1046. password = d.wallet_passwd,
  1047. seed = xmrseed().fromhex(d.sec.wif,tostr=True),
  1048. restore_height = restore_height,
  1049. language = 'English' )
  1050. pp_msg(ret) if self.cfg.debug else msg(f' Address: {ret["address"]}')
  1051. return True
  1052. class create_offline(create):
  1053. def __init__(self,cfg,uarg_tuple):
  1054. super().__init__(cfg,uarg_tuple)
  1055. gmsg('\nCreating viewkey-address file for watch-only wallets')
  1056. vkal = ViewKeyAddrList(
  1057. cfg = self.cfg,
  1058. proto = self.proto,
  1059. addrfile = None,
  1060. addr_idxs = uarg.wallets,
  1061. seed = self.seed_src.seed,
  1062. skip_chksum_msg = True )
  1063. vkf = vkal.file
  1064. # before writing viewkey-address file, shred any old ones in the directory:
  1065. for f in Path(self.asi.xmr_dir).iterdir():
  1066. if f.name.endswith(vkf.ext):
  1067. from .fileutil import shred_file
  1068. msg(f"\nShredding old viewkey-address file '{f}'")
  1069. shred_file( f, verbose=self.cfg.verbose )
  1070. vkf.write(outdir=self.asi.xmr_dir)
  1071. class restore(create):
  1072. wallet_offline = True
  1073. def check_uopts(self):
  1074. if self.cfg.restore_height is not None:
  1075. die(1,'--restore-height must be unset when running the ‘restore’ command')
  1076. async def process_wallet(self,d,fn,last):
  1077. def get_dump_data():
  1078. def gen():
  1079. for fn in [self.get_wallet_fn(d,watch_only=wo) for wo in (True,False)]:
  1080. ret = fn.parent / (fn.name + '.dump')
  1081. if ret.exists():
  1082. yield ret
  1083. dump_fns = tuple(gen())
  1084. if not dump_fns:
  1085. die(1,f"No suitable dump file found for '{fn}'")
  1086. elif len(dump_fns) > 1:
  1087. ymsg(f"Warning: more than one dump file found for '{fn}' - using the first!")
  1088. return MoneroWalletDumpFile.Completed(
  1089. parent = self,
  1090. fn = dump_fns[0] ).data._asdict()['wallet_metadata']
  1091. def restore_accounts():
  1092. bmsg(' Restoring accounts:')
  1093. for acct_idx,acct_data in enumerate(data[1:],1):
  1094. msg(fs.format(acct_idx, 0, acct_data['address']))
  1095. self.c.call('create_account')
  1096. def restore_subaddresses():
  1097. bmsg(' Restoring subaddresses:')
  1098. for acct_idx,acct_data in enumerate(data):
  1099. for addr_idx,addr_data in enumerate(acct_data['addresses'][1:],1):
  1100. msg(fs.format(acct_idx, addr_idx, addr_data['address']))
  1101. self.c.call( 'create_address', account_index=acct_idx )
  1102. def restore_labels():
  1103. bmsg(' Restoring labels:')
  1104. for acct_idx,acct_data in enumerate(data):
  1105. for addr_idx,addr_data in enumerate(acct_data['addresses']):
  1106. addr_data['used'] = False # do this so that restored data matches
  1107. msg(fs.format(acct_idx, addr_idx, addr_data['label']))
  1108. self.c.call(
  1109. 'label_address',
  1110. index = { 'major': acct_idx, 'minor': addr_idx },
  1111. label = addr_data['label'],
  1112. )
  1113. def make_format_str():
  1114. return ' acct {:O>%s}, addr {:O>%s} [{}]' % (
  1115. len(str( len(data) - 1 )),
  1116. len(str( max(len(acct_data['addresses']) for acct_data in data) - 1))
  1117. )
  1118. def check_restored_data():
  1119. restored_data = h.get_accts(print=False)[1]
  1120. if restored_data != data:
  1121. rmsg('Restored data does not match original dump! Dumping bad data.')
  1122. MoneroWalletDumpFile.New(
  1123. parent = self,
  1124. wallet_fn = fn,
  1125. data = {'wallet_metadata': restored_data} ).write(add_suf='.bad')
  1126. die(3,'Fatal error')
  1127. await super().process_wallet(d,fn,last)
  1128. h = self.rpc(self,d)
  1129. h.open_wallet('newly created')
  1130. msg('')
  1131. data = get_dump_data()
  1132. fs = make_format_str()
  1133. gmsg('\nRestoring accounts, subaddresses and labels from dump file:\n')
  1134. restore_accounts()
  1135. restore_subaddresses()
  1136. restore_labels()
  1137. check_restored_data()
  1138. return True
  1139. class sync(wallet):
  1140. opts = ('rescan_blockchain',)
  1141. def check_uopts(self):
  1142. if self.cfg.rescan_blockchain and self.cfg.watch_only:
  1143. die(1,f'Operation {self.name!r} does not support --rescan-blockchain with watch-only wallets')
  1144. def __init__(self,cfg,uarg_tuple):
  1145. super().__init__(cfg,uarg_tuple)
  1146. if not self.wallet_offline:
  1147. self.dc = self.get_coin_daemon_rpc()
  1148. self.accts_data = {}
  1149. async def process_wallet(self,d,fn,last):
  1150. chain_height = self.dc.call_raw('get_height')['height']
  1151. msg(f' Chain height: {chain_height}')
  1152. t_start = time.time()
  1153. msg_r(' Opening wallet...')
  1154. self.c.call(
  1155. 'open_wallet',
  1156. filename = fn.name,
  1157. password = d.wallet_passwd )
  1158. msg('done')
  1159. msg_r(' Getting wallet height (be patient, this could take a long time)...')
  1160. wallet_height = self.c.call('get_height')['height']
  1161. msg_r('\r' + ' '*68 + '\r')
  1162. msg(f' Wallet height: {wallet_height} ')
  1163. behind = chain_height - wallet_height
  1164. if behind > 1000:
  1165. msg_r(f' Wallet is {behind} blocks behind chain tip. Please be patient. Syncing...')
  1166. ret = self.c.call('refresh')
  1167. if behind > 1000:
  1168. msg('done')
  1169. if ret['received_money']:
  1170. msg(' Wallet has received funds')
  1171. for i in range(2):
  1172. wallet_height = self.c.call('get_height')['height']
  1173. if wallet_height >= chain_height:
  1174. break
  1175. ymsg(f' Wallet failed to sync (wallet height [{wallet_height}] < chain height [{chain_height}])')
  1176. if i or not self.cfg.rescan_blockchain:
  1177. break
  1178. msg_r(' Rescanning blockchain, please be patient...')
  1179. self.c.call('rescan_blockchain')
  1180. self.c.call('refresh')
  1181. msg('done')
  1182. t_elapsed = int(time.time() - t_start)
  1183. a,b = self.rpc(self,d).get_accts(print=False)
  1184. msg(' Balance: {} Unlocked balance: {}'.format(
  1185. hl_amt(a['total_balance']),
  1186. hl_amt(a['total_unlocked_balance']),
  1187. ))
  1188. self.accts_data[fn.name] = { 'accts': a, 'addrs': b }
  1189. msg(f' Wallet height: {wallet_height}')
  1190. msg(f' Sync time: {t_elapsed//60:02}:{t_elapsed%60:02}')
  1191. if not last:
  1192. self.c.call('close_wallet')
  1193. return wallet_height >= chain_height
  1194. def post_main_success(self):
  1195. d = self.accts_data
  1196. def gen_info():
  1197. for wnum,k in enumerate(d):
  1198. if self.name in ('sync', 'view'):
  1199. yield from self.rpc(self, self.addr_data[wnum]).gen_accts_info(
  1200. d[k]['accts'],
  1201. d[k]['addrs'],
  1202. indent = '')
  1203. yield ''
  1204. elif self.name in ('list', 'listview'):
  1205. fs = ' {:2} {} {} {}'
  1206. yield green(f'Wallet {k}:')
  1207. for acct_num,acct in enumerate(d[k]['addrs']):
  1208. yield ''
  1209. yield ' Account #{a} [{b} {c}]'.format(
  1210. a = acct_num,
  1211. b = self.proto.coin_amt(
  1212. d[k]['accts']['subaddress_accounts'][acct_num]['unlocked_balance'],
  1213. from_unit='atomic').hl(),
  1214. c = self.proto.coin_amt.hlc('XMR')
  1215. )
  1216. yield fs.format('', 'Address'.ljust(95), 'Used ', 'Label')
  1217. for addr in acct['addresses']:
  1218. yield fs.format(
  1219. addr['address_index'],
  1220. CoinAddr(self.proto, addr['address']).hl(),
  1221. (yellow('True ') if addr['used'] else green('False')),
  1222. pink(addr['label']))
  1223. yield ''
  1224. col1_w = max(map(len, d)) + 1
  1225. fs = '{:%s} {} {}' % col1_w
  1226. tbals = [0, 0]
  1227. yield fs.format('Wallet', 'Balance ', 'Unlocked Balance')
  1228. for k in d:
  1229. b = d[k]['accts']['total_balance']
  1230. ub = d[k]['accts']['total_unlocked_balance']
  1231. yield fs.format(k + ':', fmt_amt(b), fmt_amt(ub))
  1232. tbals[0] += b
  1233. tbals[1] += ub
  1234. yield fs.format('-'*col1_w, '-'*18, '-'*18)
  1235. yield fs.format('TOTAL:', fmt_amt(tbals[0]), fmt_amt(tbals[1]))
  1236. self.cfg._util.stdout_or_pager('\n'.join(gen_info()) + '\n')
  1237. class list(sync):
  1238. stem = 'sync'
  1239. class view(sync):
  1240. stem = 'open'
  1241. opts = ()
  1242. wallet_offline = True
  1243. def pre_init_action(self):
  1244. ymsg('WARNING: Running in offline mode. Balances and other info may be out of date!')
  1245. async def process_wallet(self,d,fn,last):
  1246. self.c.call(
  1247. 'open_wallet',
  1248. filename = fn.name,
  1249. password = d.wallet_passwd)
  1250. wallet_height = self.c.call('get_height')['height']
  1251. msg(f' Wallet height: {wallet_height}')
  1252. a,b = self.rpc(self, d).get_accts(print=False)
  1253. self.accts_data[fn.name] = {'accts': a, 'addrs': b}
  1254. if not last:
  1255. self.c.call('close_wallet')
  1256. return True
  1257. class listview(view):
  1258. pass
  1259. class spec(wallet): # virtual class
  1260. def create_addr_data(self):
  1261. m = re.fullmatch(uarg_info[self.spec_id].pat,uarg.spec,re.ASCII)
  1262. if not m:
  1263. fs = "{!r}: invalid {!r} arg: for {} operation, it must have format {!r}"
  1264. die(1,fs.format( uarg.spec, self.spec_id, self.name, uarg_info[self.spec_id].annot ))
  1265. def gen():
  1266. for i,k in self.spec_key:
  1267. if m[i] is None:
  1268. setattr(self,k,None)
  1269. else:
  1270. idx = int(m[i])
  1271. try:
  1272. res = self.kal.entry(idx)
  1273. except:
  1274. die(1,f'Supplied key-address file does not contain address {self.kal.al_id.sid}:{idx}')
  1275. else:
  1276. setattr(self,k,res)
  1277. yield res
  1278. self.addr_data = list(gen())
  1279. self.account = None if m[2] is None else int(m[2])
  1280. def strip_quotes(s):
  1281. if s and s[0] in ("'",'"'):
  1282. if s[-1] != s[0] or len(s) < 2:
  1283. die(1,f'{s!r}: unbalanced quotes in label string!')
  1284. return s[1:-1]
  1285. else:
  1286. return s # None or empty string
  1287. if self.name == 'transfer':
  1288. self.dest_addr = CoinAddr(self.proto,m[3])
  1289. self.amount = self.proto.coin_amt(m[4])
  1290. elif self.name == 'new':
  1291. self.label = strip_quotes(m[3])
  1292. elif self.name == 'label':
  1293. self.address_idx = int(m[3])
  1294. self.label = strip_quotes(m[4])
  1295. class sweep(spec):
  1296. spec_id = 'sweep_spec'
  1297. spec_key = ( (1,'source'), (3,'dest') )
  1298. opts = ('no_relay','tx_relay_daemon','watch_only','priority')
  1299. def check_uopts(self):
  1300. if self.cfg.tx_relay_daemon and (self.cfg.no_relay or self.cfg.autosign):
  1301. die(1,'--tx-relay-daemon makes no sense in this context!')
  1302. if self.cfg.priority and self.cfg.priority not in list(tx_priorities):
  1303. die(1, '{}: invalid parameter for --priority (valid params: {})'.format(
  1304. self.cfg.priority,
  1305. fmt_dict(tx_priorities, fmt='square_compact')))
  1306. def init_tx_relay_daemon(self):
  1307. m = self.parse_tx_relay_opt()
  1308. wd2 = MoneroWalletDaemon(
  1309. cfg = self.cfg,
  1310. proto = self.proto,
  1311. wallet_dir = self.cfg.wallet_dir or '.',
  1312. test_suite = self.cfg.test_suite,
  1313. monerod_addr = m[1],
  1314. proxy = m[2] )
  1315. if self.cfg.test_suite:
  1316. wd2.usr_daemon_args = ['--daemon-ssl-allow-any-cert']
  1317. wd2.start()
  1318. self.c = MoneroWalletRPCClient(
  1319. cfg = self.cfg,
  1320. daemon = wd2 )
  1321. def create_tx(self, h, accts_data):
  1322. if self.dest is None:
  1323. dest_acct = self.account
  1324. if keypress_confirm(self.cfg, f'\nCreate new address for account #{self.account}?'):
  1325. dest_addr_chk = h.create_new_addr(self.account)
  1326. elif keypress_confirm(self.cfg, f'Sweep to last existing address of account #{self.account}?'):
  1327. dest_addr_chk = None
  1328. else:
  1329. die(1,'Exiting at user request')
  1330. dest_addr, dest_addr_idx = h.get_last_addr(self.account, display=not dest_addr_chk)
  1331. assert dest_addr_chk in (None, dest_addr), 'dest_addr_chk1'
  1332. h.print_addrs(accts_data, self.account)
  1333. else:
  1334. h.close_wallet('source')
  1335. h2 = self.rpc(self, self.dest)
  1336. h2.open_wallet('destination')
  1337. wf = self.get_wallet_fn(self.dest)
  1338. if keypress_confirm(self.cfg, f'\nCreate new account for wallet {wf.name!r}?'):
  1339. dest_acct, dest_addr = h2.create_acct(
  1340. label = f'Sweep from {self.source.idx}:{self.account} [{make_timestr()}]')
  1341. dest_addr_idx = 0
  1342. h2.get_accts()
  1343. elif keypress_confirm(self.cfg, f'Sweep to last existing account of wallet {wf.name!r}?'):
  1344. dest_acct, dest_addr_chk = h2.get_last_acct(h2.get_accts()[0])
  1345. dest_addr, dest_addr_idx = h2.get_last_addr(dest_acct, display=False)
  1346. assert dest_addr_chk == dest_addr, 'dest_addr_chk2'
  1347. else:
  1348. die(1, 'Exiting at user request')
  1349. h2.close_wallet('destination')
  1350. h.open_wallet('source', refresh=False)
  1351. msg(f'\n Creating {self.name} transaction...')
  1352. return (h, h.make_sweep_tx(self.account, dest_acct, dest_addr_idx, dest_addr))
  1353. @property
  1354. def add_desc(self):
  1355. if self.dest is None:
  1356. return ' to new address'
  1357. else:
  1358. return f' to new account in wallet {self.dest.idx}'
  1359. async def main(self):
  1360. gmsg(
  1361. f'\n{self.stem.capitalize()}ing account #{self.account} of wallet {self.source.idx}'
  1362. + self.add_desc)
  1363. h = self.rpc(self,self.source)
  1364. h.open_wallet('source')
  1365. accts_data = h.get_accts()[0]
  1366. max_acct = len(accts_data['subaddress_accounts']) - 1
  1367. if self.account > max_acct:
  1368. die(2, f'{self.account}: requested account index out of bounds (>{max_acct})')
  1369. h.print_addrs(accts_data,self.account)
  1370. h, new_tx = self.create_tx(h, accts_data)
  1371. msg('\n' + new_tx.get_info(indent=' '))
  1372. if self.cfg.tx_relay_daemon:
  1373. self.display_tx_relay_info(indent=' ')
  1374. msg('Saving TX data to file')
  1375. new_tx.write(delete_metadata=True)
  1376. if self.cfg.no_relay or self.cfg.autosign:
  1377. return True
  1378. if keypress_confirm( self.cfg, f'Relay {self.name} transaction?' ):
  1379. if self.cfg.tx_relay_daemon:
  1380. await h.stop_wallet('source')
  1381. msg('')
  1382. self.init_tx_relay_daemon()
  1383. h = self.rpc(self,self.source)
  1384. h.open_wallet('TX-relay-configured source',refresh=False)
  1385. msg_r(f'\n Relaying {self.name} transaction...')
  1386. h.relay_tx(new_tx.data.metadata)
  1387. gmsg('\nAll done')
  1388. return True
  1389. else:
  1390. die(1,'\nExiting at user request')
  1391. class transfer(sweep):
  1392. stem = 'transferr'
  1393. spec_id = 'transfer_spec'
  1394. spec_key = ( (1,'source'), )
  1395. @property
  1396. def add_desc(self):
  1397. return f': {self.amount} XMR to {self.dest_addr}'
  1398. def create_tx(self, h, accts_data):
  1399. msg(f'\n Creating {self.name} transaction...')
  1400. return (h, h.make_transfer_tx(self.account, self.dest_addr, self.amount))
  1401. class new(spec):
  1402. spec_id = 'newaddr_spec'
  1403. spec_key = ( (1,'source'), )
  1404. wallet_offline = True
  1405. async def main(self):
  1406. h = self.rpc(self,self.source)
  1407. h.open_wallet('Monero')
  1408. desc = 'account' if self.account is None else 'address'
  1409. label = (
  1410. None if self.label == '' else
  1411. '{} [{}]'.format(self.label or f'xmrwallet new {desc}', make_timestr()))
  1412. accts_data = h.get_accts()[0]
  1413. if desc == 'address':
  1414. h.print_addrs(accts_data,self.account)
  1415. if keypress_confirm(
  1416. self.cfg,
  1417. '\nCreating new {a} for wallet {b}{c} with {d}\nOK?'.format(
  1418. a = desc,
  1419. b = red(str(self.source.idx)),
  1420. c = '' if desc == 'account' else f', account {red("#"+str(self.account))}',
  1421. d = 'label ' + pink('‘'+label+'’') if label else 'empty label')
  1422. ):
  1423. if desc == 'address':
  1424. h.create_new_addr(self.account,label=label)
  1425. else:
  1426. h.create_acct(label=label)
  1427. accts_data = h.get_accts()[0]
  1428. if desc == 'address':
  1429. h.print_addrs(accts_data,self.account)
  1430. else:
  1431. ymsg('\nOperation cancelled by user request')
  1432. # wallet must be left open: otherwise the 'stop_wallet' RPC call used to stop the daemon will fail
  1433. if self.cfg.no_stop_wallet_daemon:
  1434. h.close_wallet('Monero')
  1435. msg('')
  1436. class label(spec):
  1437. spec_id = 'label_spec'
  1438. spec_key = ( (1,'source'), )
  1439. opts = ()
  1440. wallet_offline = True
  1441. async def main(self):
  1442. gmsg('\n{a} label for wallet {b}, account #{c}, address #{d}'.format(
  1443. a = 'Setting' if self.label else 'Removing',
  1444. b = self.source.idx,
  1445. c = self.account,
  1446. d = self.address_idx
  1447. ))
  1448. h = self.rpc(self,self.source)
  1449. h.open_wallet('source')
  1450. accts_data = h.get_accts()[0]
  1451. max_acct = len(accts_data['subaddress_accounts']) - 1
  1452. if self.account > max_acct:
  1453. die(2, f'{self.account}: requested account index out of bounds (>{max_acct})')
  1454. ret = h.print_addrs(accts_data,self.account)
  1455. if self.address_idx > len(ret['addresses']) - 1:
  1456. die(2, '{}: requested address index out of bounds (>{})'.format(
  1457. self.address_idx,
  1458. len(ret['addresses']) - 1 ))
  1459. addr = ret['addresses'][self.address_idx]
  1460. new_label = f'{self.label} [{make_timestr()}]' if self.label else ''
  1461. msg('\n {a} {b}\n {c} {d}\n {e} {f}'.format(
  1462. a = 'Address: ',
  1463. b = cyan(addr['address'][:15] + '...'),
  1464. c = 'Existing label:',
  1465. d = pink(addr['label']) if addr['label'] else gray('[none]'),
  1466. e = 'New label: ',
  1467. f = pink(new_label) if new_label else gray('[none]') ))
  1468. op = 'remove' if not new_label else 'update' if addr['label'] else 'set'
  1469. if addr['label'] == new_label:
  1470. ymsg('\nLabel is unchanged, operation cancelled')
  1471. elif keypress_confirm(self.cfg, f' {op.capitalize()} label?'):
  1472. h.set_label(self.account, self.address_idx, new_label)
  1473. accts_data = h.get_accts(print=False)[0]
  1474. ret = h.print_addrs(accts_data,self.account)
  1475. label_chk = ret['addresses'][self.address_idx]['label']
  1476. if label_chk != new_label:
  1477. ymsg(f'Warning: new label {label_chk!r} does not match requested value!')
  1478. return False
  1479. else:
  1480. msg(cyan('\nLabel successfully {}'.format('set' if op == 'set' else op+'d')))
  1481. else:
  1482. ymsg('\nOperation cancelled by user request')
  1483. class sign(wallet):
  1484. action = 'signing transaction with'
  1485. start_daemon = False
  1486. async def main(self,fn,restart_daemon=True):
  1487. if restart_daemon:
  1488. await self.restart_wallet_daemon()
  1489. tx = MoneroMMGenTX.Unsigned( self.cfg, fn )
  1490. h = self.rpc(self,self.addr_data[0])
  1491. self.head_msg(tx.src_wallet_idx,h.fn)
  1492. if restart_daemon:
  1493. h.open_wallet(refresh=False)
  1494. res = self.c.call(
  1495. 'sign_transfer',
  1496. unsigned_txset = tx.data.unsigned_txset,
  1497. export_raw = True,
  1498. get_tx_keys = True
  1499. )
  1500. new_tx = MoneroMMGenTX.NewColdSigned(
  1501. cfg = self.cfg,
  1502. txid = res['tx_hash_list'][0],
  1503. unsigned_txset = None,
  1504. signed_txset = res['signed_txset'],
  1505. _in_tx = tx,
  1506. )
  1507. return new_tx
  1508. class submit(wallet):
  1509. action = 'submitting transaction with'
  1510. opts = ('tx_relay_daemon',)
  1511. def post_mount_action(self):
  1512. self.tx # trigger an exit if no suitable transaction present
  1513. @property
  1514. def tx(self):
  1515. if not hasattr(self,'_tx'):
  1516. self._tx = self.get_tx()
  1517. return self._tx
  1518. def get_tx(self):
  1519. if uarg.infile:
  1520. fn = Path(uarg.infile)
  1521. else:
  1522. from .autosign import Signable
  1523. fn = Signable.xmr_transaction(self.asi).get_unsubmitted()
  1524. return MoneroMMGenTX.ColdSigned(cfg=self.cfg, fn=fn)
  1525. def get_relay_rpc(self):
  1526. relay_opt = self.parse_tx_relay_opt()
  1527. wd = MoneroWalletDaemon(
  1528. cfg = self.cfg,
  1529. proto = self.proto,
  1530. wallet_dir = self.cfg.wallet_dir or '.',
  1531. test_suite = self.cfg.test_suite,
  1532. monerod_addr = relay_opt[1],
  1533. )
  1534. u = wd.usr_daemon_args = []
  1535. if self.cfg.test_suite:
  1536. u.append('--daemon-ssl-allow-any-cert')
  1537. if relay_opt[2]:
  1538. u.append(f'--proxy={relay_opt[2]}')
  1539. return MoneroWalletRPCClient(
  1540. cfg = self.cfg,
  1541. daemon = wd,
  1542. test_connection = False,
  1543. )
  1544. async def main(self):
  1545. tx = self.tx
  1546. h = self.rpc( self, self.kal.entry(tx.src_wallet_idx) )
  1547. self.head_msg(tx.src_wallet_idx,h.fn)
  1548. h.open_wallet()
  1549. if self.cfg.tx_relay_daemon:
  1550. await self.c.stop_daemon()
  1551. self.c = self.get_relay_rpc()
  1552. self.c.start_daemon()
  1553. h = self.rpc( self, self.kal.entry(tx.src_wallet_idx) )
  1554. h.open_wallet( 'TX-relay-configured watch-only', refresh=False )
  1555. msg('\n' + tx.get_info(indent=' '))
  1556. if self.cfg.tx_relay_daemon:
  1557. self.display_tx_relay_info(indent=' ')
  1558. if keypress_confirm( self.cfg, f'{self.name.capitalize()} transaction?' ):
  1559. if self.cfg.tx_relay_daemon:
  1560. msg_r('Relaying transaction to remote daemon, please be patient...')
  1561. res = self.c.call(
  1562. 'submit_transfer',
  1563. tx_data_hex = tx.data.signed_txset )
  1564. assert res['tx_hash_list'][0] == tx.data.txid, 'TxID mismatch in ‘submit_transfer’ result!'
  1565. if self.cfg.tx_relay_daemon:
  1566. msg('success')
  1567. else:
  1568. die(1,'Exiting at user request')
  1569. new_tx = MoneroMMGenTX.NewSubmitted(
  1570. cfg = self.cfg,
  1571. _in_tx = tx,
  1572. )
  1573. gmsg('\nOK')
  1574. new_tx.write(
  1575. ask_write = not self.cfg.autosign,
  1576. ask_overwrite = not self.cfg.autosign )
  1577. return new_tx
  1578. class resubmit(submit):
  1579. action = 'resubmitting transaction with'
  1580. def check_uopts(self):
  1581. if not self.cfg.autosign:
  1582. die(1,'--autosign is required for this operation')
  1583. def get_tx(self):
  1584. from .autosign import Signable
  1585. fns = Signable.xmr_transaction(self.asi).get_submitted()
  1586. return sorted(
  1587. (MoneroMMGenTX.Submitted(self.cfg, Path(fn)) for fn in fns),
  1588. key = lambda x: getattr(x.data,'submit_time',None) or x.data.create_time
  1589. )[-1]
  1590. class abort(base):
  1591. opts = ('watch_only','autosign')
  1592. def __init__(self, cfg, uarg_tuple):
  1593. super().__init__(cfg,uarg_tuple)
  1594. self.mount_removable_device()
  1595. from .autosign import Signable
  1596. Signable.xmr_transaction(self.asi).shred_abortable() # prompts user, then raises exception or exits
  1597. class dump(wallet):
  1598. wallet_offline = True
  1599. async def process_wallet(self,d,fn,last):
  1600. h = self.rpc(self,d)
  1601. h.open_wallet('source')
  1602. _,addr_data = h.get_accts(print=False)
  1603. msg('')
  1604. MoneroWalletDumpFile.New(
  1605. parent = self,
  1606. wallet_fn = fn,
  1607. data = {'wallet_metadata': addr_data} ).write()
  1608. return True
  1609. class export_outputs(wallet):
  1610. action = 'exporting outputs from'
  1611. stem = 'process'
  1612. opts = ('rescan_blockchain',)
  1613. sign = False
  1614. async def process_wallet(self,d,fn,last):
  1615. h = self.rpc(self,d)
  1616. h.open_wallet('source')
  1617. if self.cfg.rescan_blockchain:
  1618. gmsg_r('\n Rescanning blockchain...')
  1619. self.c.call('rescan_blockchain')
  1620. gmsg('done')
  1621. self.head_msg(d.idx,h.fn)
  1622. for ftype in ('Unsigned','Signed'):
  1623. old_fn = getattr(MoneroWalletOutputsFile,ftype).find_fn_from_wallet_fn(
  1624. cfg = self.cfg,
  1625. wallet_fn = fn,
  1626. ret_on_no_match = True )
  1627. if old_fn:
  1628. old_fn.unlink()
  1629. m = MoneroWalletOutputsFile.New(
  1630. parent = self,
  1631. wallet_fn = fn,
  1632. data = self.c.call('export_outputs', all=True),
  1633. sign = self.sign,
  1634. )
  1635. m.write()
  1636. return True
  1637. class export_outputs_sign(export_outputs):
  1638. sign = True
  1639. class import_outputs(wallet):
  1640. action = 'importing wallet outputs into'
  1641. start_daemon = False
  1642. async def main(self,fn,wallet_idx,restart_daemon=True):
  1643. if restart_daemon:
  1644. await self.restart_wallet_daemon()
  1645. h = self.rpc(self,self.addr_data[0])
  1646. self.head_msg(wallet_idx,fn)
  1647. if restart_daemon:
  1648. h.open_wallet(refresh=False)
  1649. m = MoneroWalletOutputsFile.Unsigned(
  1650. parent = self,
  1651. fn = fn )
  1652. res = self.c.call(
  1653. 'import_outputs',
  1654. outputs_data_hex = m.data.outputs_data_hex )
  1655. idata = res['num_imported']
  1656. bmsg(f'\n {idata} output{suf(idata)} imported')
  1657. if m.data.sign:
  1658. data = m.data._asdict()
  1659. data.update(self.c.call('export_key_images', all=True))
  1660. m = MoneroWalletOutputsFile.SignedNew(
  1661. parent = self,
  1662. wallet_fn = m.get_wallet_fn(fn),
  1663. data = data)
  1664. idata = m.data.signed_key_images or []
  1665. bmsg(f' {len(idata)} key image{suf(idata)} signed')
  1666. else:
  1667. m.data = m.data._replace(imported=True)
  1668. return m
  1669. class import_key_images(wallet):
  1670. action = 'importing key images into'
  1671. stem = 'process'
  1672. trust_monerod = True
  1673. def post_main_failure(self):
  1674. rw_msg = ' for requested wallets' if uarg.wallets else ''
  1675. die(2, f'No signed key image files found{rw_msg}!')
  1676. async def process_wallet(self,d,fn,last):
  1677. keyimage_fn = MoneroWalletOutputsFile.Signed.find_fn_from_wallet_fn( self.cfg, fn, ret_on_no_match=True )
  1678. if not keyimage_fn:
  1679. msg(f'No signed key image file found for wallet #{d.idx}')
  1680. return False
  1681. h = self.rpc(self,d)
  1682. h.open_wallet()
  1683. self.head_msg(d.idx,h.fn)
  1684. m = MoneroWalletOutputsFile.Signed( parent=self, fn=keyimage_fn )
  1685. data = m.data.signed_key_images or []
  1686. bmsg(f'\n {len(data)} signed key image{suf(data)} to import')
  1687. if data:
  1688. res = self.c.call( 'import_key_images', signed_key_images=data )
  1689. bmsg(f' Success: {res}')
  1690. return True
  1691. class relay(base):
  1692. opts = ('tx_relay_daemon',)
  1693. def __init__(self,cfg,uarg_tuple):
  1694. super().__init__(cfg,uarg_tuple)
  1695. self.mount_removable_device()
  1696. self.tx = MoneroMMGenTX.Signed( self.cfg, Path(uarg.infile) )
  1697. if self.cfg.tx_relay_daemon:
  1698. m = self.parse_tx_relay_opt()
  1699. host,port = m[1].split(':')
  1700. proxy = m[2]
  1701. md = None
  1702. else:
  1703. from .daemon import CoinDaemon
  1704. md = CoinDaemon( self.cfg, 'xmr', test_suite=self.cfg.test_suite )
  1705. host,port = ('localhost', md.rpc_port)
  1706. proxy = None
  1707. self.dc = MoneroRPCClient(
  1708. cfg = self.cfg,
  1709. proto = self.proto,
  1710. daemon = md,
  1711. host = host,
  1712. port = int(port),
  1713. user = None,
  1714. passwd = None,
  1715. test_connection = host == 'localhost', # avoid extra connections if relay is a public node
  1716. proxy = proxy )
  1717. async def main(self):
  1718. msg('\n' + self.tx.get_info(indent=' '))
  1719. if self.cfg.tx_relay_daemon:
  1720. self.display_tx_relay_info(indent=' ')
  1721. if keypress_confirm( self.cfg, 'Relay transaction?' ):
  1722. res = self.dc.call_raw(
  1723. 'send_raw_transaction',
  1724. tx_as_hex = self.tx.data.blob
  1725. )
  1726. if res['status'] == 'OK':
  1727. msg('Status: ' + green('OK'))
  1728. if res['not_relayed']:
  1729. ymsg('Transaction not relayed')
  1730. return True
  1731. else:
  1732. die( 'RPCFailure', repr(res) )
  1733. else:
  1734. die(1,'Exiting at user request')
  1735. class txview(base):
  1736. view_method = 'get_info'
  1737. opts = ('watch_only','autosign')
  1738. hdr = ''
  1739. col_hdr = ''
  1740. footer = ''
  1741. cols = None
  1742. do_umount = False
  1743. async def main(self,cols=None):
  1744. self.mount_removable_device()
  1745. if self.cfg.autosign:
  1746. files = [f for f in self.asi.xmr_tx_dir.iterdir()
  1747. if f.name.endswith('.'+MoneroMMGenTX.Submitted.ext)]
  1748. else:
  1749. files = uarg.infile
  1750. txs = sorted(
  1751. (MoneroMMGenTX.View( self.cfg, Path(fn) ) for fn in files),
  1752. # old TX files have no ‘submit_time’ field:
  1753. key = lambda x: getattr(x.data,'submit_time',None) or x.data.create_time
  1754. )
  1755. if self.cfg.autosign:
  1756. self.asi.do_umount()
  1757. self.cfg._util.stdout_or_pager(
  1758. (self.hdr if len(files) > 1 else '')
  1759. + self.col_hdr
  1760. + '\n'.join(getattr(tx,self.view_method)(cols=cols) for tx in txs)
  1761. + self.footer
  1762. )
  1763. class txlist(txview):
  1764. view_method = 'get_info_oneline'
  1765. add_nl = True
  1766. footer = '\n'
  1767. @property
  1768. def hdr(self):
  1769. return ('SUBMITTED ' if self.cfg.autosign else '') + 'MONERO TRANSACTIONS\n'
  1770. @property
  1771. def col_hdr(self):
  1772. return MoneroMMGenTX.View.oneline_fs.format(
  1773. a = 'Network',
  1774. b = 'Seed ID',
  1775. c = 'Submitted' if self.cfg.autosign else 'Date',
  1776. d = 'TxID',
  1777. e = 'Type',
  1778. f = 'Src',
  1779. g = 'Dest',
  1780. h = ' Amount',
  1781. j = 'Dest Address',
  1782. x = '',
  1783. ) + '\n'
  1784. async def main(self):
  1785. if self.cfg.pager:
  1786. cols = None
  1787. else:
  1788. from .term import get_terminal_size
  1789. cols = self.cfg.columns or get_terminal_size().width
  1790. if cols < 106:
  1791. die(1, 'A terminal at least 106 columns wide is required to display this output'
  1792. + ' (or use --columns or --pager)' )
  1793. await super().main(cols=cols)