mmgen-xmrwallet: TX files, offline TX creation & signing
- create transactions offline using the --do-not-relay option
- send transaction files with the `relay` operation
A monerod with a fully synced blockchain must be running on the offline
machine.
For more details, type `mmgen-xmrwallet --help`
Testing:
test/test.py --coin=xmr -e xmrwallet
This commit is contained in:
parent
369724ea0b
commit
3d8ee62e5a
3 changed files with 229 additions and 11 deletions
|
|
@ -33,12 +33,15 @@ opts_data = {
|
|||
'[opts] sync <xmr_keyaddrfile> [wallets]',
|
||||
'[opts] transfer <xmr_keyaddrfile> <transfer_spec>',
|
||||
'[opts] sweep <xmr_keyaddrfile> <sweep_spec>',
|
||||
'[opts] relay <TX_file>',
|
||||
],
|
||||
'options': """
|
||||
-h, --help Print this help message
|
||||
--, --longhelp Print help message for long options (common
|
||||
options)
|
||||
-b, --rescan-blockchain Rescan the blockchain if wallet fails to sync
|
||||
-d, --outdir=D Save transaction files to directory 'D'
|
||||
instead of the working directory
|
||||
-D, --daemon=H:P Connect to the monerod at {D}
|
||||
-R, --tx-relay-daemon=H:P[:H:P] Relay transactions via a monerod specified by
|
||||
{R}
|
||||
|
|
@ -46,6 +49,7 @@ opts_data = {
|
|||
-p, --hash-preset=P Use scrypt hash preset 'P' for password
|
||||
hashing (default: '{g.dfl_hash_preset}')
|
||||
-r, --restore-height=H Scan from height 'H' when creating wallets
|
||||
-R, --do-not-relay Save transaction to file instead of relaying
|
||||
-s, --no-start-wallet-daemon Don’t start the wallet daemon at startup
|
||||
-S, --no-stop-wallet-daemon Don’t stop the wallet daemon at exit
|
||||
-w, --wallet-dir=D Output or operate on wallets in directory 'D'
|
||||
|
|
@ -72,6 +76,8 @@ 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
|
||||
relay - relay a transaction from a transaction file created using 'sweep'
|
||||
or 'transfer' with the --do-not-relay option
|
||||
|
||||
|
||||
CREATE AND SYNC OPERATION NOTES
|
||||
|
|
@ -113,11 +119,21 @@ The user is prompted before addresses are created or funds are transferred.
|
|||
Note that multiple sweep operations may be required to sweep all the funds
|
||||
in an account.
|
||||
|
||||
|
||||
RELAY OPERATION NOTES
|
||||
|
||||
By default, transactions are relayed to a monerod running on localhost at the
|
||||
default RPC port. To relay transactions to a remote or non-default monerod
|
||||
via optional SOCKS proxy, use the --tx-relay-daemon option described above.
|
||||
|
||||
|
||||
WARNING
|
||||
|
||||
Note that the use of this command requires private data to be exposed on a
|
||||
network-connected machine in order to unlock the Monero wallets. This is a
|
||||
violation of good security practice.
|
||||
To avoid exposing your private keys on a network-connected machine, you’re
|
||||
strongly advised to create all transactions offline using the --do-not-relay
|
||||
option. For this, a monerod with a fully synced blockchain must be running
|
||||
on the offline machine. The resulting transaction files are then sent using
|
||||
the 'relay' operation.
|
||||
|
||||
|
||||
EXAMPLES
|
||||
|
|
@ -142,6 +158,13 @@ $ mmgen-xmrwallet sweep *.akeys.mmenc 1:0,2
|
|||
|
||||
Send 0.1 XMR from account #0 of wallet 2 to an external address:
|
||||
$ mmgen-xmrwallet transfer *.akeys.mmenc 2:0:<monero address>,0.1
|
||||
|
||||
Sweep all funds from account #0 of wallet 2 to a new address, saving the
|
||||
transaction to a file:
|
||||
$ mmgen-xmrwallet --do-not-relay sweep *.akeys.mmenc 2:0
|
||||
|
||||
Relay the created sweep transaction via a host on the Tor network:
|
||||
$ mmgen-xmrwallet --tx-relay-daemon=abcdefghijklmnop.onion:127.0.0.1:9050 relay *XMR*.sigtx
|
||||
"""
|
||||
},
|
||||
'code': {
|
||||
|
|
@ -166,7 +189,10 @@ if op not in MoneroWalletOps.ops:
|
|||
|
||||
wallets = spec = ''
|
||||
|
||||
if op in ('create','sync'):
|
||||
if op == 'relay':
|
||||
if len(cmd_args) != 0:
|
||||
opts.usage()
|
||||
elif op in ('create','sync'):
|
||||
if len(cmd_args) not in (0,1):
|
||||
opts.usage()
|
||||
if cmd_args:
|
||||
|
|
@ -184,6 +210,7 @@ uo = namedtuple('uopts',[
|
|||
'rescan_blockchain',
|
||||
'no_start_wallet_daemon',
|
||||
'no_stop_wallet_daemon',
|
||||
'do_not_relay',
|
||||
'wallet_dir',
|
||||
])
|
||||
|
||||
|
|
@ -195,6 +222,7 @@ uopts = uo(
|
|||
opt.rescan_blockchain,
|
||||
opt.no_start_wallet_daemon,
|
||||
opt.no_stop_wallet_daemon,
|
||||
opt.do_not_relay,
|
||||
opt.wallet_dir,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@
|
|||
xmrwallet.py - MoneroWalletOps class
|
||||
"""
|
||||
|
||||
import os,re,time
|
||||
import os,re,time,json
|
||||
from collections import namedtuple
|
||||
from .common import *
|
||||
from .addr import KeyAddrList,AddrIdxList
|
||||
from .rpc import MoneroRPCClientRaw, MoneroWalletRPCClient
|
||||
from .rpc import MoneroRPCClientRaw,MoneroWalletRPCClient,json_encoder
|
||||
from .daemon import MoneroWalletDaemon
|
||||
from .protocol import _b58a,init_proto
|
||||
from .obj import CoinAddr,CoinTxID,SeedID,AddrIdx,Hilite,InitErrors
|
||||
|
|
@ -77,6 +77,23 @@ class MoneroMMGenTX:
|
|||
|
||||
class Base:
|
||||
|
||||
def make_chksum(self,keys=None):
|
||||
res = json.dumps(
|
||||
dict( (k,v) for k,v in self.data._asdict().items() if (not keys or k in keys) ),
|
||||
cls = json_encoder
|
||||
)
|
||||
return make_chksum_6(res)
|
||||
|
||||
@property
|
||||
def base_chksum(self):
|
||||
return self.make_chksum(
|
||||
('op','create_time','network','seed_id','source','dest','amount')
|
||||
)
|
||||
|
||||
@property
|
||||
def full_chksum(self):
|
||||
return self.make_chksum(set(self.data._fields) - {'metadata'})
|
||||
|
||||
xmrwallet_tx_data = namedtuple('xmrwallet_tx_data',[
|
||||
'op',
|
||||
'create_time',
|
||||
|
|
@ -123,6 +140,29 @@ class MoneroMMGenTX:
|
|||
d.dest_address.hl()
|
||||
)
|
||||
|
||||
def write(self,delete_metadata=False):
|
||||
dict_data = self.data._asdict()
|
||||
if delete_metadata:
|
||||
dict_data['metadata'] = None
|
||||
|
||||
out = json.dumps(
|
||||
{ 'MoneroMMGenTX': {
|
||||
'base_chksum': self.base_chksum,
|
||||
'full_chksum': self.full_chksum,
|
||||
'data': dict_data,
|
||||
}
|
||||
},
|
||||
cls = json_encoder,
|
||||
indent = 4 # DEBUG
|
||||
)
|
||||
fn = '{}{}-XMR[{!s}]{}.sigtx'.format(
|
||||
self.base_chksum.upper(),
|
||||
('-'+self.full_chksum.upper()) if self.full_chksum else '',
|
||||
self.data.amount,
|
||||
(lambda s: '' if s == 'mainnet' else f'.{s}')(self.data.network),
|
||||
)
|
||||
write_data_to_file(fn,out,desc='MoneroMMGenTX data',ask_write=True,ask_write_default_yes=False)
|
||||
|
||||
class NewSigned(Base):
|
||||
|
||||
def __init__(self,*args,**kwargs):
|
||||
|
|
@ -146,9 +186,36 @@ class MoneroMMGenTX:
|
|||
metadata = d.metadata,
|
||||
)
|
||||
|
||||
class Signed(Base):
|
||||
|
||||
def __init__(self,fn):
|
||||
self.fn = fn
|
||||
d_wrap = json.loads(get_data_from_file(fn))['MoneroMMGenTX']
|
||||
d = self.xmrwallet_tx_data(**d_wrap['data'])
|
||||
proto = init_proto('xmr',network=d.network)
|
||||
self.data = self.xmrwallet_tx_data(
|
||||
op = d.op,
|
||||
create_time = d.create_time,
|
||||
sign_time = d.sign_time,
|
||||
network = d.network,
|
||||
seed_id = SeedID(sid=d.seed_id),
|
||||
source = XMRWalletAddrSpec(d.source),
|
||||
dest = None if d.dest is None else XMRWalletAddrSpec(d.dest),
|
||||
dest_address = CoinAddr(proto,d.dest_address),
|
||||
txid = CoinTxID(d.txid),
|
||||
amount = proto.coin_amt(d.amount),
|
||||
fee = proto.coin_amt(d.fee),
|
||||
blob = d.blob,
|
||||
metadata = d.metadata,
|
||||
)
|
||||
for k in ('base_chksum','full_chksum'):
|
||||
a = getattr(self,k)
|
||||
b = d_wrap[k]
|
||||
assert a == b, f'{k} mismatch: {a} != {b}'
|
||||
|
||||
class MoneroWalletOps:
|
||||
|
||||
ops = ('create','sync','transfer','sweep')
|
||||
ops = ('create','sync','transfer','sweep','relay')
|
||||
opts = (
|
||||
'wallet_dir',
|
||||
'daemon',
|
||||
|
|
@ -158,6 +225,7 @@ class MoneroWalletOps:
|
|||
'restore_height',
|
||||
'no_start_wallet_daemon',
|
||||
'no_stop_wallet_daemon',
|
||||
'do_not_relay',
|
||||
)
|
||||
pat_opts = ('daemon','tx_relay_daemon')
|
||||
|
||||
|
|
@ -618,7 +686,7 @@ class MoneroWalletOps:
|
|||
past = 'swept'
|
||||
spec_id = 'sweep_spec'
|
||||
spec_key = ( (1,'source'), (3,'dest') )
|
||||
opts = ('tx_relay_daemon',)
|
||||
opts = ('do_not_relay','tx_relay_daemon')
|
||||
|
||||
def create_addr_data(self):
|
||||
m = re.fullmatch(uarg_info[self.spec_id].pat,uarg.spec,re.ASCII)
|
||||
|
|
@ -737,7 +805,10 @@ class MoneroWalletOps:
|
|||
if uopt.tx_relay_daemon:
|
||||
self.display_tx_relay_info(indent=' ')
|
||||
|
||||
if keypress_confirm(f'Relay {self.name} transaction?'):
|
||||
if uopt.do_not_relay:
|
||||
msg('Saving TX data to file')
|
||||
new_tx.write(delete_metadata=True)
|
||||
elif keypress_confirm(f'Relay {self.name} transaction?'):
|
||||
w_desc = 'source'
|
||||
if uopt.tx_relay_daemon:
|
||||
await h.close_wallet('source')
|
||||
|
|
@ -763,3 +834,53 @@ class MoneroWalletOps:
|
|||
past = 'transferred'
|
||||
spec_id = 'transfer_spec'
|
||||
spec_key = ( (1,'source'), )
|
||||
|
||||
class relay(base):
|
||||
name = 'relay'
|
||||
desc = 'Relay'
|
||||
past = 'relayed'
|
||||
opts = ('tx_relay_daemon',)
|
||||
|
||||
def __init__(self,uarg_tuple,uopt_tuple):
|
||||
|
||||
super().__init__(uarg_tuple,uopt_tuple)
|
||||
|
||||
if uopt.tx_relay_daemon:
|
||||
m = re.fullmatch(uarg_info['tx_relay_daemon'].pat,uopt.tx_relay_daemon,re.ASCII)
|
||||
host,port = m[1].split(':')
|
||||
proxy = m[2]
|
||||
else:
|
||||
from .daemon import CoinDaemon
|
||||
md = CoinDaemon('xmr',test_suite=g.test_suite)
|
||||
host,port = md.host,md.rpc_port
|
||||
proxy = None
|
||||
|
||||
self.dc = MoneroRPCClientRaw(
|
||||
host = host,
|
||||
port = int(port),
|
||||
user = None,
|
||||
passwd = None,
|
||||
proxy = proxy )
|
||||
|
||||
self.tx = MoneroMMGenTX.Signed(uarg.infile)
|
||||
|
||||
async def main(self):
|
||||
msg('\n' + self.tx.get_info())
|
||||
|
||||
if uopt.tx_relay_daemon:
|
||||
self.display_tx_relay_info()
|
||||
|
||||
if keypress_confirm('Relay transaction?'):
|
||||
res = await self.dc.call(
|
||||
'send_raw_transaction',
|
||||
tx_as_hex = self.tx.data.blob
|
||||
)
|
||||
if res['status'] == 'OK':
|
||||
msg('Status: ' + green('OK'))
|
||||
if res['not_relayed']:
|
||||
ymsg('Transaction not relayed')
|
||||
return True
|
||||
else:
|
||||
raise RPCFailure(repr(res))
|
||||
else:
|
||||
die(1,'Exiting at user request')
|
||||
|
|
|
|||
|
|
@ -56,6 +56,13 @@ class TestSuiteXMRWallet(TestSuiteBase):
|
|||
('sweep_to_address_noproxy', 'sweeping to new address (via TX relay, no proxy)'),
|
||||
('transfer_to_miner_proxy', 'transferring funds to Miner (via TX relay + proxy)'),
|
||||
('transfer_to_miner_noproxy', 'transferring funds to Miner (via TX relay, no proxy)'),
|
||||
|
||||
('transfer_to_miner_create1', 'transferring funds to Miner (create TX)'),
|
||||
('transfer_to_miner_send1', 'transferring funds to Miner (send TX via proxy)'),
|
||||
('transfer_to_miner_create2', 'transferring funds to Miner (create TX)'),
|
||||
('transfer_to_miner_send2', 'transferring funds to Miner (send TX, no proxy)'),
|
||||
|
||||
('sweep_create_and_send', 'sweeping to new account (create TX + send TX, in stages)'),
|
||||
)
|
||||
|
||||
def __init__(self,trunner,cfgs,spawn):
|
||||
|
|
@ -343,6 +350,7 @@ class TestSuiteXMRWallet(TestSuiteBase):
|
|||
|
||||
def do_op(self, op, user, arg2,
|
||||
tx_relay_parm = None,
|
||||
do_not_relay = False,
|
||||
return_amt = False,
|
||||
reuse_acct = False,
|
||||
add_desc = None,
|
||||
|
|
@ -353,7 +361,8 @@ class TestSuiteXMRWallet(TestSuiteBase):
|
|||
[f'--wallet-dir={data.udir}'],
|
||||
[f'--outdir={data.udir}'],
|
||||
[f'--daemon=localhost:{data.md.rpc_port}'],
|
||||
[f'--tx-relay-daemon={tx_relay_parm}', tx_relay_parm]
|
||||
[f'--tx-relay-daemon={tx_relay_parm}', tx_relay_parm],
|
||||
['--do-not-relay', do_not_relay]
|
||||
)
|
||||
add_desc = (', ' + add_desc) if add_desc else ''
|
||||
|
||||
|
|
@ -375,7 +384,12 @@ class TestSuiteXMRWallet(TestSuiteBase):
|
|||
if return_amt:
|
||||
amt = XMRAmt(strip_ansi_escapes(t.expect_getend('Amt: ')).replace('XMR','').strip())
|
||||
|
||||
t.expect(f'Relay {op} transaction? (y/N): ','y')
|
||||
if do_not_relay:
|
||||
t.expect('Save MoneroMMGenTX data? (y/N): ','y')
|
||||
t.written_to_file('MoneroMMGenTX data')
|
||||
else:
|
||||
t.expect(f'Relay {op} transaction? (y/N): ','y')
|
||||
|
||||
t.read()
|
||||
|
||||
return t if do_ret else amt if return_amt else t.ok()
|
||||
|
|
@ -403,6 +417,61 @@ class TestSuiteXMRWallet(TestSuiteBase):
|
|||
self.do_op('transfer','alice',f'2:1:{addr},0.0995',self.tx_relay_daemon_parm)
|
||||
return self.mine_chk('miner',2,0,lambda x: str(x) == '0.2345','unlocked balance == 0.2345')
|
||||
|
||||
def transfer_to_miner_create(self,amt):
|
||||
get_file_with_ext(self.users['alice'].udir,'sigtx',delete_all=True)
|
||||
addr = read_from_file(self.users['miner'].addrfile_fs.format(2))
|
||||
return self.do_op('transfer','alice',f'2:1:{addr},{amt}',do_not_relay=True,do_ret=True)
|
||||
|
||||
def transfer_to_miner_create1(self):
|
||||
return self.transfer_to_miner_create('0.0111')
|
||||
|
||||
def transfer_to_miner_create2(self):
|
||||
return self.transfer_to_miner_create('0.0012')
|
||||
|
||||
def relay_tx(self,relay_opt=None,add_desc=None):
|
||||
user = 'alice'
|
||||
data = self.users[user]
|
||||
fn = get_file_with_ext(data.udir,'sigtx')
|
||||
add_desc = (', ' + add_desc) if add_desc else ''
|
||||
t = self.spawn(
|
||||
'mmgen-xmrwallet',
|
||||
self.long_opts
|
||||
+ ([relay_opt] if relay_opt else [])
|
||||
+ [ 'relay', fn ],
|
||||
extra_desc = f'(relaying TX, {capfirst(user)}{add_desc})' )
|
||||
t.expect('Relay transaction? ','y')
|
||||
t.read()
|
||||
t.ok()
|
||||
|
||||
def transfer_to_miner_send1(self):
|
||||
self.relay_tx(f'--tx-relay-daemon={self.tx_relay_daemon_proxy_parm}',add_desc='via proxy')
|
||||
return self.mine_chk('miner',2,0,lambda x: str(x) == '0.2456','unlocked balance == 0.2456')
|
||||
|
||||
def transfer_to_miner_send2(self):
|
||||
self.relay_tx(f'--tx-relay-daemon={self.tx_relay_daemon_parm}',add_desc='no proxy')
|
||||
return self.mine_chk('miner',2,0,lambda x: str(x) == '0.2468','unlocked balance == 0.2468')
|
||||
|
||||
async def sweep_create_and_send(self):
|
||||
bal = XMRAmt('0')
|
||||
min_bal = XMRAmt('0.9')
|
||||
|
||||
for i in range(4):
|
||||
if i: ok()
|
||||
get_file_with_ext(self.users['alice'].udir,'sigtx',delete_all=True)
|
||||
send_amt = self.do_op(
|
||||
'sweep','alice','2:1,3', # '2:1,3'
|
||||
do_not_relay = True,
|
||||
reuse_acct = True,
|
||||
add_desc = f'TX #{i+1}',
|
||||
return_amt = True )
|
||||
ok()
|
||||
self.relay_tx(f'--tx-relay-daemon={self.tx_relay_daemon_parm}',add_desc=f'send amt: {send_amt} XMR')
|
||||
bal += await self.mine_chk('alice',3,0,lambda x: 'chk_bal_chg','balance has changed') # 3,0
|
||||
if bal >= min_bal:
|
||||
return 'ok'
|
||||
|
||||
return False
|
||||
|
||||
# wallet methods
|
||||
|
||||
async def open_wallet_user(self,user,wnum):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue