Browse Source

mmgen-xmrwallet: new `transfer` operation

The MMGen Project 3 years ago
parent
commit
4f631a1068
3 changed files with 107 additions and 21 deletions
  1. 22 8
      mmgen/main_xmrwallet.py
  2. 42 9
      mmgen/xmrwallet.py
  3. 43 4
      test/test_py_d/ts_xmrwallet.py

+ 22 - 8
mmgen/main_xmrwallet.py

@@ -29,9 +29,10 @@ opts_data = {
 		'desc': """Perform various Monero wallet operations for addresses
                    in an MMGen XMR key-address file""",
 		'usage2': [
-			'[opts] create <xmr_keyaddrfile> [wallets]',
-			'[opts] sync   <xmr_keyaddrfile> [wallets]',
-			'[opts] sweep  <xmr_keyaddrfile> <sweep_spec>',
+			'[opts] create   <xmr_keyaddrfile> [wallets]',
+			'[opts] sync     <xmr_keyaddrfile> [wallets]',
+			'[opts] transfer <xmr_keyaddrfile> <transfer_spec>',
+			'[opts] sweep    <xmr_keyaddrfile> <sweep_spec>',
 		],
 		'options': """
 -h, --help                       Print this help message
@@ -61,10 +62,12 @@ may point to a SOCKS proxy, in which case HOST may be a Tor onion address.
 
                         SUPPORTED OPERATIONS
 
-create - create wallet for all or specified addresses in key-address file
-sync   - sync wallet for all or specified addresses in key-address file
-sweep  - sweep funds in specified wallet:account to new address in same
-         account or new account in another wallet
+create    - create wallet for all or specified addresses in key-address file
+sync      - sync wallet for all or specified addresses in key-address file
+transfer  - transfer specified XMR amount to specified address from specified
+            wallet:account
+sweep     - sweep funds in specified wallet:account to new address in same
+            account or new account in another wallet
 
 
                    CREATE AND SYNC OPERATION NOTES
@@ -75,6 +78,17 @@ key-address file, each corresponding to a Monero wallet to be created or
 synced.  If omitted, all wallets are operated upon.
 
 
+                       TRANSFER OPERATION NOTES
+
+The transfer operation takes a `transfer specifier` arg with the following
+format:
+
+    SOURCE:ACCOUNT:ADDRESS,AMOUNT
+
+where SOURCE is a wallet index; ACCOUNT the source account index; and ADDRESS
+and AMOUNT the destination Monero address and XMR amount, respectively.
+
+
                         SWEEP OPERATION NOTES
 
 The sweep operation takes a `sweep specifier` arg with the following format:
@@ -126,7 +140,7 @@ if op in ('create','sync'):
 		opts.usage()
 	if cmd_args:
 		wallets = cmd_args[0]
-elif op == 'sweep':
+elif op in ('transfer','sweep'):
 	if len(cmd_args) != 1:
 		opts.usage()
 	spec = cmd_args[0]

+ 42 - 9
mmgen/xmrwallet.py

@@ -26,11 +26,13 @@ from .common import *
 from .addr import KeyAddrList,AddrIdxList
 from .rpc import MoneroRPCClientRaw, MoneroWalletRPCClient
 from .daemon import MoneroWalletDaemon
+from .protocol import _b58a
 
 xmrwallet_uarg_info = (
 	lambda e,hp: {
 		'daemon':          e('HOST:PORT', hp),
 		'tx_relay_daemon': e('HOST:PORT[:PROXY_HOST:PROXY_PORT]', r'({p})(?::({p}))?'.format(p=hp)),
+		'transfer_spec':   e('SOURCE_WALLET_NUM:ACCOUNT:ADDRESS,AMOUNT', rf'(\d+):(\d+):([{_b58a}]+),([0-9.]+)'),
 		'sweep_spec':      e('SOURCE_WALLET_NUM:ACCOUNT[,DEST_WALLET_NUM]', r'(\d+):(\d+)(?:,(\d+))?'),
 	})(
 		namedtuple('uarg_info_entry',['annot','pat']),
@@ -39,7 +41,7 @@ xmrwallet_uarg_info = (
 
 class MoneroWalletOps:
 
-	ops = ('create','sync','sweep')
+	ops = ('create','sync','transfer','sweep')
 
 	class base:
 
@@ -155,6 +157,20 @@ class MoneroWalletOps:
 					hl_amt(fee),
 				))
 
+			async def make_transfer_tx(self,account,addr,amt):
+				res = await self.c.call(
+					'transfer',
+					account_index = account,
+					destinations = [{
+						'amount':  amt.to_unit('atomic'),
+						'address': addr
+					}],
+					do_not_relay = True,
+					get_tx_metadata = True
+				)
+				self.display_tx( res['tx_hash'], res['amount'], res['fee'] )
+				return res['tx_metadata']
+
 			async def make_sweep_tx(self,account,addr):
 				res = await self.c.call(
 					'sweep_all',
@@ -438,6 +454,11 @@ class MoneroWalletOps:
 			self.addr_data = list(gen())
 			self.account = int(m[2])
 
+			if self.name == 'transfer':
+				from mmgen.obj import CoinAddr
+				self.dest_addr = CoinAddr(self.proto,m[3])
+				self.amount = self.proto.coin_amt(m[4])
+
 		def post_init(self):
 
 			if uarg.tx_relay_daemon:
@@ -467,7 +488,8 @@ class MoneroWalletOps:
 
 		async def process_wallets(self):
 			gmsg(f'\n{self.desc}ing account #{self.account} of wallet {self.source.idx}' + (
-				' to new address' if self.dest is None
+				f': {self.amount} XMR to {self.dest_addr}' if self.name == 'transfer'
+				else ' to new address' if self.dest == None
 				else f' to new account in wallet {self.dest.idx}' ))
 
 			h = self.rpc(self,self.source)
@@ -481,7 +503,9 @@ class MoneroWalletOps:
 
 			await h.get_addrs(accts_data,self.account)
 
-			if self.dest == None:
+			if self.name == 'transfer':
+				new_addr = self.dest_addr
+			elif self.dest == None:
 				if keypress_confirm(f'\nCreate new address for account #{self.account}?'):
 					new_addr = await h.create_new_addr(self.account)
 				elif keypress_confirm(f'Sweep to last existing address of account #{self.account}?'):
@@ -507,12 +531,14 @@ class MoneroWalletOps:
 				await h2.close_wallet('destination')
 				await h.open_wallet('source')
 
-			msg('\n    Creating sweep transaction: balance of wallet {}, account #{} => {}'.format(
-				self.source.idx,
-				self.account,
-				cyan(new_addr),
-			))
-			tx_metadata = await h.make_sweep_tx(self.account,new_addr)
+			msg_r(f'\n    Creating {self.name} transaction: wallet {self.source.idx}, account #{self.account}')
+
+			if self.name == 'transfer':
+				msg(f', {self.amount} XMR => {cyan(new_addr)}')
+				tx_metadata = await h.make_transfer_tx(self.account,new_addr,self.amount)
+			elif self.name == 'sweep':
+				msg(f' => {cyan(new_addr)}')
+				tx_metadata = await h.make_sweep_tx(self.account,new_addr)
 
 			if keypress_confirm(f'\nRelay {self.name} transaction?'):
 				w_desc = 'source'
@@ -533,3 +559,10 @@ class MoneroWalletOps:
 				die(1,'\nExiting at user request')
 
 			return True
+
+	class transfer(sweep):
+		name    = 'transfer'
+		desc    = 'Transfer'
+		past    = 'transferred'
+		spec_id = 'transfer_spec'
+		spec_key = ( (1,'source'), )

+ 43 - 4
test/test_py_d/ts_xmrwallet.py

@@ -61,6 +61,13 @@ class TestSuiteXMRWallet(TestSuiteBase):
 
 		('sweep_to_address_noproxy',  'sweeping to new address (via TX relay)'),
 		('mine_blocks',               'mining blocks'),
+
+		('transfer_to_miner_proxy',   'transferring funds to Miner (via TX relay + proxy)'),
+		('mine_blocks_extra',         'mining blocks'),
+		('sync_wallet_2',             'syncing Alice’s wallet #2'),
+
+		('transfer_to_miner_noproxy', 'transferring funds to Miner (via TX relay)'),
+		('mine_blocks',               'mining blocks'),
 	)
 
 	def __init__(self,trunner,cfgs,spawn):
@@ -197,7 +204,7 @@ class TestSuiteXMRWallet(TestSuiteBase):
 			'wd_rpc',
 		])
 		for user,sid,shift,kal_range in ( # kal_range must be None, a single digit, or a single hyphenated range
-				('miner', '98831F3A', 130,  '1'),
+				('miner', '98831F3A', 130,  '1-2'),
 				('bob',   '1378FC64', 140, None),
 				('alice', 'FE3C6545', 150, '1-4'),
 			):
@@ -320,6 +327,9 @@ class TestSuiteXMRWallet(TestSuiteBase):
 	def sync_wallets_selected(self):
 		return self.sync_wallets(wallets='1,3-4')
 
+	def sync_wallet_2(self):
+		return self.sync_wallets(wallets='2')
+
 	def sync_wallets(self,wallets=None):
 		data = self.users['alice']
 		dir_opt = [f'--outdir={data.udir}']
@@ -376,6 +386,19 @@ class TestSuiteXMRWallet(TestSuiteBase):
 		self.set_dest('alice',2,1,lambda x: x > 1,'unlocked balance > 1')
 		return ret
 
+	def transfer_to_miner_proxy(self):
+		addr = read_from_file(self.users['miner'].addrfile_fs.format(2))
+		amt = '0.135'
+		ret = self.do_op('transfer','alice',f'2:1:{addr},{amt}',self.tx_relay_daemon_proxy_parm)
+		self.set_dest('miner',2,0,lambda x: str(x) == amt,f'unlocked balance == {amt}')
+		return ret
+
+	def transfer_to_miner_noproxy(self):
+		addr = read_from_file(self.users['miner'].addrfile_fs.format(2))
+		ret = self.do_op('transfer','alice',f'2:1:{addr},0.0995',self.tx_relay_daemon_parm)
+		self.set_dest('miner',2,0,lambda x: str(x) == '0.2345','unlocked balance == 0.2345')
+		return ret
+
 	# wallet methods
 
 	async def open_wallet_user(self,user,wnum):
@@ -420,7 +443,7 @@ class TestSuiteXMRWallet(TestSuiteBase):
 		ret = await self.users['miner'].md_rpc.call('stop_mining')
 		return self.get_status(ret)
 
-	async def mine_blocks(self,random_txs=None):
+	async def mine_blocks(self,random_txs=None,extra_blocks=None):
 		"""
 		- open destination wallet
 		- optionally create and broadcast random TXs
@@ -447,6 +470,18 @@ class TestSuiteXMRWallet(TestSuiteBase):
 			else:
 				die(2,'Restart attempt limit exceeded')
 
+		async def mine_extra_blocks():
+			h_start = await get_height()
+			imsg_r(f'[+{extra_blocks} blocks]: ')
+			oqmsg_r('|')
+			while True:
+				await asyncio.sleep(2)
+				h = await get_height()
+				imsg_r(f'{h} ')
+				oqmsg_r('+')
+				if h - h_start > extra_blocks:
+					break
+
 		async def send_random_txs():
 			from mmgen.tool import tool_api
 			t = tool_api()
@@ -480,8 +515,7 @@ class TestSuiteXMRWallet(TestSuiteBase):
 
 		self.do_msg(extra_desc=f'+{random_txs} random TXs' if random_txs else None)
 
-		if self.dest.user != 'miner':
-			await self.open_wallet_user(self.dest.user,self.dest.wnum)
+		await self.open_wallet_user(self.dest.user,self.dest.wnum)
 
 		if random_txs:
 			await send_random_txs()
@@ -494,6 +528,8 @@ class TestSuiteXMRWallet(TestSuiteBase):
 		while True:
 			ub = await get_balance(self.dest)
 			if self.dest.test(ub):
+				if extra_blocks:
+					await mine_extra_blocks()
 				imsg('')
 				oqmsg_r('+')
 				print_balance(self.dest,ub)
@@ -515,6 +551,9 @@ class TestSuiteXMRWallet(TestSuiteBase):
 	async def mine_blocks_tx(self):
 		return await self.mine_blocks(random_txs=self.dfl_random_txs)
 
+	async def mine_blocks_extra(self):
+		return await self.mine_blocks(extra_blocks=100) # TODO: 100 is arbitrary value
+
 	# util methods
 
 	def get_status(self,ret):