|
@@ -21,45 +21,29 @@
|
|
|
tx.py: Transaction routines for the MMGen suite
|
|
|
"""
|
|
|
|
|
|
-import sys,os
|
|
|
+import sys,os,json
|
|
|
from stat import *
|
|
|
from binascii import unhexlify
|
|
|
from mmgen.common import *
|
|
|
from mmgen.obj import *
|
|
|
|
|
|
-pnm = g.proj_name
|
|
|
-
|
|
|
-wmsg = {
|
|
|
+wmsg = lambda k: {
|
|
|
'addr_in_addrfile_only': """
|
|
|
-Warning: output address {mmgenaddr} is not in the tracking wallet, which means
|
|
|
+Warning: output address {} is not in the tracking wallet, which means
|
|
|
its balance will not be tracked. You're strongly advised to import the address
|
|
|
into your tracking wallet before broadcasting this transaction.
|
|
|
""".strip(),
|
|
|
'addr_not_found': """
|
|
|
-No data for {pnm} address {mmgenaddr} could be found in either the tracking
|
|
|
+No data for {pnm} address {{}} could be found in either the tracking
|
|
|
wallet or the supplied address file. Please import this address into your
|
|
|
tracking wallet, or supply an address file for it on the command line.
|
|
|
-""".strip(),
|
|
|
+""".strip().format(pnm=g.proj_name),
|
|
|
'addr_not_found_no_addrfile': """
|
|
|
-No data for {pnm} address {mmgenaddr} could be found in the tracking wallet.
|
|
|
+No data for {pnm} address {{}} could be found in the tracking wallet.
|
|
|
Please import this address into your tracking wallet or supply an address file
|
|
|
for it on the command line.
|
|
|
-""".strip(),
|
|
|
- 'non_mmgen_inputs': """
|
|
|
-NOTE: This transaction includes non-{pnm} inputs, which makes the signing
|
|
|
-process more complicated. When signing the transaction, keys for non-{pnm}
|
|
|
-inputs must be supplied to '{pnl}-txsign' in a file with the '--keys-from-file'
|
|
|
-option.
|
|
|
-Selected non-{pnm} inputs: {{}}
|
|
|
-""".strip().format(pnm=pnm,pnl=pnm.lower()),
|
|
|
- 'not_enough_coin': """
|
|
|
-Selected outputs insufficient to fund this transaction ({{}} {} needed)
|
|
|
-""".strip().format(g.coin),
|
|
|
- 'no_change_output': """
|
|
|
-ERROR: No change address specified. If you wish to create a transaction with
|
|
|
-only one output, specify a single output address with no {} amount
|
|
|
-""".strip().format(g.coin),
|
|
|
-}
|
|
|
+""".strip().format(pnm=g.proj_name),
|
|
|
+}[k]
|
|
|
|
|
|
def strfmt_locktime(num,terse=False):
|
|
|
# Locktime itself is an unsigned 4-byte integer which can be parsed two ways:
|
|
@@ -89,13 +73,13 @@ def mmaddr2coinaddr(mmaddr,ad_w,ad_f):
|
|
|
if ad_f:
|
|
|
coin_addr = ad_f.mmaddr2coinaddr(mmaddr)
|
|
|
if coin_addr:
|
|
|
- msg(wmsg['addr_in_addrfile_only'].format(mmgenaddr=mmaddr))
|
|
|
+ msg(wmsg('addr_in_addrfile_only').format(mmaddr))
|
|
|
if not keypress_confirm('Continue anyway?'):
|
|
|
sys.exit(1)
|
|
|
else:
|
|
|
- die(2,wmsg['addr_not_found'].format(pnm=pnm,mmgenaddr=mmaddr))
|
|
|
+ die(2,wmsg('addr_not_found').format(mmaddr))
|
|
|
else:
|
|
|
- die(2,wmsg['addr_not_found_no_addrfile'].format(pnm=pnm,mmgenaddr=mmaddr))
|
|
|
+ die(2,wmsg('addr_not_found_no_addrfile').format(mmaddr))
|
|
|
|
|
|
return CoinAddr(coin_addr)
|
|
|
|
|
@@ -235,8 +219,22 @@ class MMGenTX(MMGenObject):
|
|
|
rel_fee_disp = 'satoshis per byte'
|
|
|
txview_hdr_fs = 'TRANSACTION DATA\n\nID={i} ({a} {c}) UTC={t} RBF={r} Sig={s} Locktime={l}\n'
|
|
|
txview_hdr_fs_short = 'TX {i} ({a} {c}) UTC={t} RBF={r} Sig={s} Locktime={l}\n'
|
|
|
+ txview_ftr_fs = 'Total input: {i} {d}\nTotal output: {o} {d}\nTX fee: {a} {c}{r}\n'
|
|
|
+ txview_ftr_fs_short = 'In {i} {d} - Out {o} {d}\nFee {a} {c}{r}\n'
|
|
|
usr_fee_prompt = 'Enter transaction fee: '
|
|
|
|
|
|
+ msg_low_coin = 'Selected outputs insufficient to fund this transaction ({} {} needed)'
|
|
|
+ msg_no_change_output = """
|
|
|
+ERROR: No change address specified. If you wish to create a transaction with
|
|
|
+only one output, specify a single output address with no {} amount
|
|
|
+""".strip()
|
|
|
+ msg_non_mmgen_inputs = """
|
|
|
+NOTE: This transaction includes non-{pnm} inputs, which makes the signing
|
|
|
+process more complicated. When signing the transaction, keys for non-{pnm}
|
|
|
+inputs must be supplied to '{pnl}-txsign' in a file with the '--keys-from-file'
|
|
|
+option.
|
|
|
+Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_name.lower())
|
|
|
+
|
|
|
class MMGenTxInput(MMGenListItem):
|
|
|
for k in txio_attrs: locals()[k] = txio_attrs[k] # in lieu of inheritance
|
|
|
scriptPubKey = MMGenListItemAttr('scriptPubKey','HexStr')
|
|
@@ -1065,7 +1063,7 @@ class MMGenTX(MMGenObject):
|
|
|
sel_f = lambda o: len(o.mmid) + (2,8)[bool(o.is_chg)] # + len(' (chg)')
|
|
|
return max(max([sel_f(o) for o in io if o.mmid] or [0]),len(nonmm_str))
|
|
|
|
|
|
- nonmm_str = '(non-{pnm} address)'.format(pnm=g.proj_name)
|
|
|
+ nonmm_str = '(non-{} address)'.format(g.proj_name)
|
|
|
max_mmwid = max(get_max_mmwid(self.inputs),get_max_mmwid(self.outputs))
|
|
|
|
|
|
out = (self.txview_hdr_fs,self.txview_hdr_fs_short)[bool(terse)].format(
|
|
@@ -1088,17 +1086,12 @@ class MMGenTX(MMGenObject):
|
|
|
|
|
|
out += self.format_view_body(blockcount,nonmm_str,max_mmwid,enl,terse=terse)
|
|
|
|
|
|
- fs = (
|
|
|
- 'Total input: {i} {c}\nTotal output: {o} {c}\nTX fee: {a} {c}{r}\n',
|
|
|
- 'In {i} {c} - Out {o} {c}\nFee {a} {c}{r}\n'
|
|
|
- )[bool(terse)]
|
|
|
-
|
|
|
- out += fs.format(
|
|
|
+ out += (self.txview_ftr_fs,self.txview_ftr_fs_short)[bool(terse)].format(
|
|
|
i=self.sum_inputs().hl(),
|
|
|
o=self.sum_outputs().hl(),
|
|
|
a=self.format_view_abs_fee(),
|
|
|
r=self.format_view_rel_fee(terse),
|
|
|
- c=g.coin)
|
|
|
+ d='WIP',c=g.coin)
|
|
|
|
|
|
if opt.verbose: out += self.format_view_verbose_footer()
|
|
|
|
|
@@ -1182,7 +1175,7 @@ class MMGenTX(MMGenObject):
|
|
|
self.check_tx_hex_data()
|
|
|
# the following ops will all fail if g.coin doesn't match self.coin
|
|
|
desc = 'coin type in metadata'
|
|
|
- assert self.coin == g.coin,'invalid coin type: {}'.format(self.coin)
|
|
|
+ assert self.coin == g.coin,self.coin
|
|
|
desc = 'inputs data'
|
|
|
self.inputs = eval_io_data(inputs_data,'inputs')
|
|
|
desc = 'outputs data'
|
|
@@ -1190,7 +1183,7 @@ class MMGenTX(MMGenObject):
|
|
|
except Exception as e:
|
|
|
die(2,'Invalid {} in transaction file: {}'.format(desc,e[0]))
|
|
|
|
|
|
- # test doesn't work for Ethereum
|
|
|
+ # test doesn't work for Ethereum: test and mainnet addrs have same format
|
|
|
if not self.chain and not self.inputs[0].addr.is_for_chain('testnet'):
|
|
|
self.chain = 'mainnet'
|
|
|
|
|
@@ -1212,7 +1205,8 @@ class MMGenTX(MMGenObject):
|
|
|
die(2,'{}: invalid command-line argument'.format(a))
|
|
|
|
|
|
if self.get_chg_output_idx() == None:
|
|
|
- die(2,('ERROR: No change output specified',wmsg['no_change_output'])[len(self.outputs) == 1])
|
|
|
+ die(2,( 'ERROR: No change output specified',
|
|
|
+ self.msg_no_change_output.format(g.dcoin))[len(self.outputs) == 1])
|
|
|
|
|
|
if not segwit_is_active() and self.has_segwit_outputs():
|
|
|
fs = '{} Segwit address requested on the command line, but Segwit is not active on this chain'
|
|
@@ -1249,6 +1243,18 @@ class MMGenTX(MMGenObject):
|
|
|
return selected
|
|
|
msg('Unspent output number must be <= {}'.format(len(unspent)))
|
|
|
|
|
|
+ def check_sufficient_funds(self,inputs,foo):
|
|
|
+ if self.send_amt > inputs:
|
|
|
+ msg(self.msg_low_coin.format(self.send_amt-inputs,g.coin))
|
|
|
+ return False
|
|
|
+ return True
|
|
|
+
|
|
|
+ def get_change_amt(self):
|
|
|
+ return self.sum_inputs() - self.send_amt - self.fee
|
|
|
+
|
|
|
+ def warn_insufficient_chg(self,change_amt):
|
|
|
+ msg(self.msg_low_coin.format(g.proto.coin_amt(-change_amt).hl(),g.coin))
|
|
|
+
|
|
|
def get_inputs_from_user(self,tw):
|
|
|
|
|
|
while True:
|
|
@@ -1258,13 +1264,13 @@ class MMGenTX(MMGenObject):
|
|
|
sel_unspent = tw.MMGenTwOutputList([tw.unspent[i-1] for i in sel_nums])
|
|
|
|
|
|
t_inputs = sum(s.amt for s in sel_unspent)
|
|
|
- if t_inputs < self.send_amt:
|
|
|
- msg(wmsg['not_enough_coin'].format(self.send_amt-t_inputs))
|
|
|
+ if not self.check_sufficient_funds(t_inputs,sel_unspent):
|
|
|
continue
|
|
|
|
|
|
non_mmaddrs = [i for i in sel_unspent if i.twmmid.type == 'non-mmgen']
|
|
|
if non_mmaddrs and self.caller != 'txdo':
|
|
|
- msg(wmsg['non_mmgen_inputs'].format(', '.join(set(sorted([a.addr.hl() for a in non_mmaddrs])))))
|
|
|
+ msg(self.msg_non_mmgen_inputs.format(
|
|
|
+ ', '.join(set(sorted([a.addr.hl() for a in non_mmaddrs])))))
|
|
|
if not keypress_confirm('Accept?'):
|
|
|
continue
|
|
|
|
|
@@ -1272,7 +1278,7 @@ class MMGenTX(MMGenObject):
|
|
|
|
|
|
self.fee = self.get_fee_from_user()
|
|
|
|
|
|
- change_amt = self.sum_inputs() - self.send_amt - self.fee
|
|
|
+ change_amt = self.get_change_amt()
|
|
|
|
|
|
if change_amt >= 0:
|
|
|
p = self.chg_msg_fs.format(change_amt.hl(),g.coin)
|
|
@@ -1280,7 +1286,7 @@ class MMGenTX(MMGenObject):
|
|
|
if opt.yes: msg(p)
|
|
|
return change_amt
|
|
|
else:
|
|
|
- msg(wmsg['not_enough_coin'].format(abs(change_amt)))
|
|
|
+ self.warn_insufficient_chg(change_amt)
|
|
|
|
|
|
def check_fee(self):
|
|
|
assert self.sum_inputs() - self.sum_outputs() <= g.proto.max_tx_fee
|
|
@@ -1362,7 +1368,7 @@ class MMGenBumpTX(MMGenTX):
|
|
|
if not self.marked_signed():
|
|
|
die(1,"File '{}' is not a signed {} transaction file".format(filename,g.proj_name))
|
|
|
if not self.coin_txid:
|
|
|
- die(1,"Transaction '{}' was not broadcast to the network".format(self.txid,g.proj_name))
|
|
|
+ die(1,"Transaction '{}' was not broadcast to the network".format(self.txid))
|
|
|
|
|
|
self.coin_txid = ''
|
|
|
self.mark_raw()
|
|
@@ -1455,7 +1461,7 @@ class MMGenSplitTX(MMGenTX):
|
|
|
if opt.yes: msg(p)
|
|
|
break
|
|
|
else:
|
|
|
- msg(wmsg['not_enough_coin'].format(abs(change_amt)))
|
|
|
+ self.warn_insufficient_chg(change_amt)
|
|
|
|
|
|
self.update_output_amt(0,change_amt)
|
|
|
self.send_amt = change_amt
|