|
@@ -46,7 +46,7 @@ class MMGenTxInputOldFmt(MMGenListItem): # for converting old tx files only
|
|
|
attrs_priv = 'tr',
|
|
|
|
|
|
class MMGenTxInput(MMGenListItem):
|
|
|
- attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','have_wif'
|
|
|
+ attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','have_wif','sequence'
|
|
|
label = MMGenListItemAttr('label','MMGenAddrLabel')
|
|
|
|
|
|
class MMGenTxOutput(MMGenListItem):
|
|
@@ -60,16 +60,11 @@ class MMGenTX(MMGenObject):
|
|
|
txid_ext = 'txid'
|
|
|
desc = 'transaction'
|
|
|
|
|
|
- max_fee = BTCAmt('0.01')
|
|
|
-
|
|
|
def __init__(self,filename=None):
|
|
|
self.inputs = []
|
|
|
self.inputs_enc = []
|
|
|
self.outputs = []
|
|
|
self.outputs_enc = []
|
|
|
- self.change_addr = ''
|
|
|
- self.size = 0 # size of raw serialized tx
|
|
|
- self.fee = BTCAmt('0')
|
|
|
self.send_amt = BTCAmt('0') # total amt minus change
|
|
|
self.hex = '' # raw serialized hex transaction
|
|
|
self.label = MMGenTXLabel('')
|
|
@@ -84,17 +79,27 @@ class MMGenTX(MMGenObject):
|
|
|
self.mark_signed()
|
|
|
self.parse_tx_file(filename)
|
|
|
|
|
|
- def add_output(self,btcaddr,amt): # 'txid','vout','amount','label','mmid','address'
|
|
|
- self.outputs.append(MMGenTxOutput(addr=btcaddr,amt=amt))
|
|
|
+ def add_output(self,btcaddr,amt,is_chg=None):
|
|
|
+ self.outputs.append(MMGenTxOutput(addr=btcaddr,amt=amt,is_chg=is_chg))
|
|
|
|
|
|
- def del_output(self,btcaddr):
|
|
|
+ def get_chg_output_idx(self):
|
|
|
for i in range(len(self.outputs)):
|
|
|
- if self.outputs[i].addr == btcaddr:
|
|
|
- self.outputs.pop(i); return
|
|
|
- raise ValueError
|
|
|
+ if self.outputs[i].is_chg == True:
|
|
|
+ return i
|
|
|
+ return None
|
|
|
+
|
|
|
+ def update_output_amt(self,idx,amt):
|
|
|
+ o = self.outputs[idx].__dict__
|
|
|
+ o['amt'] = amt
|
|
|
+ self.outputs[idx] = MMGenTxOutput(**o)
|
|
|
|
|
|
- def sum_outputs(self):
|
|
|
- return BTCAmt(sum([e.amt for e in self.outputs]))
|
|
|
+ def del_output(self,idx):
|
|
|
+ self.outputs.pop(idx)
|
|
|
+
|
|
|
+ def sum_outputs(self,exclude=None):
|
|
|
+ olist = self.outputs if exclude == None else \
|
|
|
+ self.outputs[:exclude] + self.outputs[exclude+1:]
|
|
|
+ return BTCAmt(sum([e.amt for e in olist]))
|
|
|
|
|
|
def add_mmaddrs_to_outputs(self,ad_w,ad_f):
|
|
|
a = [e.addr for e in self.outputs]
|
|
@@ -113,6 +118,8 @@ class MMGenTX(MMGenObject):
|
|
|
#
|
|
|
def create_raw(self,c):
|
|
|
i = [{'txid':e.txid,'vout':e.vout} for e in self.inputs]
|
|
|
+ if self.inputs[0].sequence:
|
|
|
+ i[0]['sequence'] = self.inputs[0].sequence
|
|
|
o = dict([(e.addr,e.amt) for e in self.outputs])
|
|
|
self.hex = c.createrawtransaction(i,o)
|
|
|
self.txid = MMGenTxID(make_chksum_6(unhexlify(self.hex)).upper())
|
|
@@ -137,31 +144,70 @@ class MMGenTX(MMGenObject):
|
|
|
def edit_comment(self):
|
|
|
return self.add_comment(self)
|
|
|
|
|
|
- # https://bitcoin.stackexchange.com/questions/1195/how-to-calculate-transaction-size-before-sending
|
|
|
- def calculate_size_and_fee(self,fee_estimate):
|
|
|
- self.size = len(self.inputs)*180 + len(self.outputs)*34 + 10
|
|
|
- if fee_estimate:
|
|
|
- ftype,fee = 'Calculated',fee_estimate*opt.tx_fee_adj*self.size / 1024
|
|
|
+ # https://bitcoin.stackexchange.com/questions/1195/
|
|
|
+ # how-to-calculate-transaction-size-before-sending
|
|
|
+ # 180: uncompressed, 148: compressed
|
|
|
+ def get_size(self):
|
|
|
+ if not self.inputs or not self.outputs: return None
|
|
|
+ return len(self.inputs)*180 + len(self.outputs)*34 + 10
|
|
|
+
|
|
|
+ def get_fee(self):
|
|
|
+ return self.sum_inputs() - self.sum_outputs()
|
|
|
+
|
|
|
+ def btc2spb(self,btc_fee):
|
|
|
+ return int(btc_fee/g.satoshi/self.get_size())
|
|
|
+
|
|
|
+ def get_relay_fee(self):
|
|
|
+ assert self.get_size()
|
|
|
+ kb_fee = BTCAmt(bitcoin_connection().getinfo()['relayfee'])
|
|
|
+ vmsg('Relay fee: {} BTC/kB'.format(kb_fee))
|
|
|
+ return kb_fee * self.get_size() / 1024
|
|
|
+
|
|
|
+ def convert_fee_spec(self,tx_fee,tx_size,on_fail='throw'):
|
|
|
+ if BTCAmt(tx_fee,on_fail='silent'):
|
|
|
+ return BTCAmt(tx_fee)
|
|
|
+ elif len(tx_fee) >= 2 and tx_fee[-1] == 's' and is_int(tx_fee[:-1]) and int(tx_fee[:-1]) >= 1:
|
|
|
+ if tx_size:
|
|
|
+ return BTCAmt(int(tx_fee[:-1]) * tx_size * g.satoshi)
|
|
|
+ else:
|
|
|
+ return None
|
|
|
else:
|
|
|
- ftype,fee = 'User-selected',opt.tx_fee
|
|
|
-
|
|
|
- ufee = None
|
|
|
- if not keypress_confirm('{} TX fee is {} BTC. OK?'.format(ftype,fee.hl()),default_yes=True):
|
|
|
- while True:
|
|
|
- ufee = my_raw_input('Enter transaction fee: ')
|
|
|
- if BTCAmt(ufee,on_fail='return'):
|
|
|
- ufee = BTCAmt(ufee)
|
|
|
- if ufee > self.max_fee:
|
|
|
- msg('{} BTC: fee too large (maximum fee: {} BTC)'.format(ufee,self.max_fee))
|
|
|
- else:
|
|
|
- fee = ufee
|
|
|
- break
|
|
|
- self.fee = fee
|
|
|
- vmsg('Inputs:{} Outputs:{} TX size:{}'.format(
|
|
|
- len(self.inputs),len(self.outputs),self.size))
|
|
|
- vmsg('Fee estimate: {} (1024 bytes, {} confs)'.format(fee_estimate,opt.tx_confs))
|
|
|
- m = ('',' (after %sx adjustment)' % opt.tx_fee_adj)[opt.tx_fee_adj != 1 and not ufee]
|
|
|
- vmsg('TX fee: {}{}'.format(self.fee,m))
|
|
|
+ if on_fail == 'return':
|
|
|
+ return False
|
|
|
+ elif on_fail == 'throw':
|
|
|
+ assert False, "'{}': invalid tx-fee argument".format(tx_fee)
|
|
|
+
|
|
|
+ def get_usr_fee(self,tx_fee,desc='Missing description'):
|
|
|
+ btc_fee = self.convert_fee_spec(tx_fee,self.get_size(),on_fail='return')
|
|
|
+ if btc_fee == None:
|
|
|
+ msg("'{}': cannot convert satoshis-per-byte to BTC because transaction size is unknown".format(tx_fee))
|
|
|
+ assert False # because we shouldn't be calling this if tx size is unknown
|
|
|
+ elif btc_fee == False:
|
|
|
+ msg("'{}': invalid TX fee (not a BTC amount or satoshis-per-byte specification)".format(tx_fee))
|
|
|
+ return False
|
|
|
+ elif btc_fee > g.max_tx_fee:
|
|
|
+ msg('{} BTC: {} fee too large (maximum fee: {} BTC)'.format(btc_fee,desc,g.max_tx_fee))
|
|
|
+ return False
|
|
|
+ elif btc_fee < self.get_relay_fee():
|
|
|
+ msg('{} BTC: {} fee too small (below relay fee of {} BTC)'.format(str(btc_fee),desc,str(self.get_relay_fee())))
|
|
|
+ return False
|
|
|
+ else:
|
|
|
+ return btc_fee
|
|
|
+
|
|
|
+ def get_usr_fee_interactive(self,tx_fee=None,desc='Starting'):
|
|
|
+ btc_fee = None
|
|
|
+ while True:
|
|
|
+ if tx_fee:
|
|
|
+ btc_fee = self.get_usr_fee(tx_fee,desc)
|
|
|
+ if btc_fee:
|
|
|
+ m = ('',' (after {}x adjustment)'.format(opt.tx_fee_adj))[opt.tx_fee_adj != 1]
|
|
|
+ p = '{} TX fee{}: {} BTC ({} satoshis per byte)'.format(desc,m,
|
|
|
+ btc_fee.hl(),pink(str(self.btc2spb(btc_fee))))
|
|
|
+ if opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
|
|
|
+ if opt.yes: msg(p)
|
|
|
+ return btc_fee
|
|
|
+ tx_fee = my_raw_input('Enter transaction fee: ')
|
|
|
+ desc = 'User-selected'
|
|
|
|
|
|
# inputs methods
|
|
|
def list_wifs(self,desc,mmaddrs_only=False):
|
|
@@ -248,27 +294,36 @@ class MMGenTX(MMGenObject):
|
|
|
msg('OK')
|
|
|
self.hex = sig_tx['hex']
|
|
|
self.mark_signed()
|
|
|
+ vmsg('Signed transaction size: {}'.format(len(self.hex)/2))
|
|
|
return True
|
|
|
else:
|
|
|
msg('failed\nBitcoind returned the following errors:')
|
|
|
pp_msg(sig_tx['errors'])
|
|
|
return False
|
|
|
|
|
|
+ def mark_raw(self):
|
|
|
+ self.desc = 'transaction'
|
|
|
+ self.ext = self.raw_ext
|
|
|
+
|
|
|
def mark_signed(self):
|
|
|
self.desc = 'signed transaction'
|
|
|
self.ext = self.sig_ext
|
|
|
|
|
|
+ def is_signed(self,color=False):
|
|
|
+ ret = self.desc == 'signed transaction'
|
|
|
+ return (red,green)[ret](str(ret)) if color else ret
|
|
|
+
|
|
|
def check_signed(self,c):
|
|
|
d = c.decoderawtransaction(self.hex)
|
|
|
ret = bool(d['vin'][0]['scriptSig']['hex'])
|
|
|
if ret: self.mark_signed()
|
|
|
return ret
|
|
|
|
|
|
- def send(self,opt,c,prompt_user=True):
|
|
|
+ def send(self,c,prompt_user=True):
|
|
|
if prompt_user:
|
|
|
m1 = ("Once this transaction is sent, there's no taking it back!",'')[bool(opt.quiet)]
|
|
|
m2 = 'broadcast this transaction to the network'
|
|
|
- m3 = ('YES, I REALLY WANT TO DO THIS','YES')[bool(opt.quiet)]
|
|
|
+ m3 = ('YES, I REALLY WANT TO DO THIS','YES')[bool(opt.quiet or opt.yes)]
|
|
|
confirm_or_exit(m1,m2,m3)
|
|
|
|
|
|
msg('Sending transaction')
|
|
@@ -276,7 +331,7 @@ class MMGenTX(MMGenObject):
|
|
|
ret = 'deadbeef' * 8
|
|
|
m = 'BOGUS transaction NOT sent: %s'
|
|
|
else:
|
|
|
- ret = c.sendrawtransaction(self.hex) # exits on failure?
|
|
|
+ ret = c.sendrawtransaction(self.hex) # exits on failure
|
|
|
m = 'Transaction sent: %s'
|
|
|
|
|
|
if ret:
|
|
@@ -288,6 +343,7 @@ class MMGenTX(MMGenObject):
|
|
|
self.add_blockcount(c)
|
|
|
return True
|
|
|
|
|
|
+ # rpc implementation exits on failure, so we won't get here
|
|
|
msg('Sending of transaction {} failed'.format(self.txid))
|
|
|
return False
|
|
|
|
|
@@ -297,12 +353,14 @@ class MMGenTX(MMGenObject):
|
|
|
ask_write=ask_write,
|
|
|
ask_write_default_yes=ask_write_default_yes)
|
|
|
|
|
|
- def write_to_file(self,add_desc='',ask_write=True,ask_write_default_yes=False,ask_tty=True):
|
|
|
+ def write_to_file(self,add_desc='',ask_write=True,ask_write_default_yes=False,ask_tty=True,ask_overwrite=True):
|
|
|
if ask_write == False:
|
|
|
ask_write_default_yes=True
|
|
|
self.format()
|
|
|
- fn = '%s[%s].%s' % (self.txid,self.send_amt,self.ext)
|
|
|
+ spbs = ('',',{}'.format(self.btc2spb(self.get_fee())))[self.is_rbf()]
|
|
|
+ fn = '{}[{}{}].{}'.format(self.txid,self.send_amt,spbs,self.ext)
|
|
|
write_data_to_file(fn,self.fmt_data,self.desc+add_desc,
|
|
|
+ ask_overwrite=ask_overwrite,
|
|
|
ask_write=ask_write,
|
|
|
ask_tty=ask_tty,
|
|
|
ask_write_default_yes=ask_write_default_yes)
|
|
@@ -314,7 +372,7 @@ class MMGenTX(MMGenObject):
|
|
|
self.view(pager=reply in 'Vv',terse=reply in 'Tt')
|
|
|
|
|
|
def view(self,pager=False,pause=True,terse=False):
|
|
|
- o = self.format_view(terse=terse).encode('utf8')
|
|
|
+ o = self.format_view(terse=terse)
|
|
|
if pager: do_pager(o)
|
|
|
else:
|
|
|
sys.stdout.write(o)
|
|
@@ -323,6 +381,21 @@ class MMGenTX(MMGenObject):
|
|
|
get_char('Press any key to continue: ')
|
|
|
msg('')
|
|
|
|
|
|
+# def is_rbf_fromhex(self,color=False):
|
|
|
+# try:
|
|
|
+# dec_tx = bitcoin_connection().decoderawtransaction(self.hex)
|
|
|
+# except:
|
|
|
+# return yellow('Unknown') if color else None
|
|
|
+# rbf = bool(dec_tx['vin'][0]['sequence'] == g.max_int - 2)
|
|
|
+# return (red,green)[rbf](str(rbf)) if color else rbf
|
|
|
+
|
|
|
+ def is_rbf(self,color=False):
|
|
|
+ ret = None < self.inputs[0].sequence <= g.max_int - 2
|
|
|
+ return (red,green)[ret](str(ret)) if color else ret
|
|
|
+
|
|
|
+ def signal_for_rbf(self):
|
|
|
+ self.inputs[0].sequence = g.max_int - 2
|
|
|
+
|
|
|
def format_view(self,terse=False):
|
|
|
try:
|
|
|
blockcount = bitcoin_connection().getblockcount()
|
|
@@ -330,11 +403,12 @@ class MMGenTX(MMGenObject):
|
|
|
blockcount = None
|
|
|
|
|
|
hdr_fs = (
|
|
|
- 'TRANSACTION DATA\n\nHeader: [Tx ID: {}] [Amount: {} BTC] [Time: {}]\n',
|
|
|
- 'Transaction {} - {} BTC - {} UTC\n'
|
|
|
+ 'TRANSACTION DATA\n\nHeader: [ID:{}] [{} BTC] [{} UTC] [RBF:{}] [Signed:{}]\n',
|
|
|
+ 'Transaction {} {} BTC ({} UTC) RBF={} Signed={}\n'
|
|
|
)[bool(terse)]
|
|
|
|
|
|
- out = hdr_fs.format(self.txid.hl(),self.send_amt.hl(),self.timestamp)
|
|
|
+ out = hdr_fs.format(self.txid.hl(),self.send_amt.hl(),self.timestamp,
|
|
|
+ self.is_rbf(color=True),self.is_signed(color=True))
|
|
|
|
|
|
enl = ('\n','')[bool(terse)]
|
|
|
if self.btc_txid: out += 'Bitcoin TxID: {}\n'.format(self.btc_txid.hl())
|
|
@@ -386,8 +460,8 @@ class MMGenTX(MMGenObject):
|
|
|
out += '\n'
|
|
|
|
|
|
fs = (
|
|
|
- 'Total input: %s BTC\nTotal output: %s BTC\nTX fee: %s BTC\n',
|
|
|
- 'In %s BTC - Out %s BTC - Fee %s BTC\n'
|
|
|
+ 'Total input: %s BTC\nTotal output: %s BTC\nTX fee: %s BTC (%s satoshis per byte)\n',
|
|
|
+ 'In %s BTC - Out %s BTC - Fee %s BTC (%s satoshis/byte)\n'
|
|
|
)[bool(terse)]
|
|
|
|
|
|
total_in = self.sum_inputs()
|
|
@@ -395,10 +469,16 @@ class MMGenTX(MMGenObject):
|
|
|
out += fs % (
|
|
|
total_in.hl(),
|
|
|
total_out.hl(),
|
|
|
- (total_in-total_out).hl()
|
|
|
+ (total_in-total_out).hl(),
|
|
|
+ pink(str(self.btc2spb(total_in-total_out))),
|
|
|
)
|
|
|
+ if opt.verbose:
|
|
|
+ ts = len(self.hex)/2 if self.hex else 'unknown'
|
|
|
+ out += 'Transaction size: estimated - {}, actual - {}\n'.format(self.get_size(),ts)
|
|
|
|
|
|
- return out
|
|
|
+ # only tx label may contain non-ascii chars
|
|
|
+ # encode() is necessary for test suite with PopenSpawn
|
|
|
+ return out.encode('utf8')
|
|
|
|
|
|
def parse_tx_file(self,infile):
|
|
|
|
|
@@ -453,3 +533,68 @@ class MMGenTX(MMGenObject):
|
|
|
|
|
|
try: self.outputs = self.decode_io('outputs',eval(outputs_data))
|
|
|
except: do_err('btc-to-mmgen address map data')
|
|
|
+
|
|
|
+class MMGenBumpTX(MMGenTX):
|
|
|
+
|
|
|
+ min_fee = None
|
|
|
+ bump_output_idx = None
|
|
|
+
|
|
|
+ def __init__(self,filename,send=False):
|
|
|
+
|
|
|
+ super(type(self),self).__init__(filename)
|
|
|
+
|
|
|
+ if not self.is_rbf():
|
|
|
+ die(1,"Transaction '{}' is not replaceable (RBF)".format(self.txid))
|
|
|
+
|
|
|
+ # If sending, require tx to have been signed and broadcast
|
|
|
+ if send:
|
|
|
+ if not self.is_signed():
|
|
|
+ die(1,"File '{}' is not a signed {} transaction file".format(filename,g.proj_name))
|
|
|
+ if not self.btc_txid:
|
|
|
+ die(1,"Transaction '{}' was not broadcast to the network".format(self.txid,g.proj_name))
|
|
|
+
|
|
|
+ self.btc_txid = ''
|
|
|
+ self.mark_raw()
|
|
|
+
|
|
|
+ def choose_output(self):
|
|
|
+ chg_idx = self.get_chg_output_idx()
|
|
|
+ init_reply = opt.output_to_reduce
|
|
|
+ while True:
|
|
|
+ if init_reply == None:
|
|
|
+ reply = my_raw_input('Which output do you wish to deduct the fee from? ')
|
|
|
+ else:
|
|
|
+ reply,init_reply = init_reply,None
|
|
|
+ if chg_idx == None and not is_int(reply):
|
|
|
+ msg("Output must be an integer")
|
|
|
+ elif chg_idx != None and not is_int(reply) and reply != 'c':
|
|
|
+ msg("Output must be an integer, or 'c' for the change output")
|
|
|
+ else:
|
|
|
+ idx = chg_idx if reply == 'c' else (int(reply) - 1)
|
|
|
+ if idx < 0 or idx >= len(self.outputs):
|
|
|
+ msg('Output must be in the range 1-{}'.format(len(self.outputs)))
|
|
|
+ else:
|
|
|
+ o_amt = self.outputs[idx].amt
|
|
|
+ cs = ('',' (change output)')[chg_idx == idx]
|
|
|
+ p = 'Fee will be deducted from output {}{} ({} BTC)'.format(idx+1,cs,o_amt)
|
|
|
+ if o_amt < self.min_fee:
|
|
|
+ msg('Minimum fee ({} BTC) is greater than output amount ({} BTC)'.format(
|
|
|
+ self.min_fee,o_amt))
|
|
|
+ elif opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
|
|
|
+ if opt.yes: msg(p)
|
|
|
+ self.bump_output_idx = idx
|
|
|
+ return idx
|
|
|
+
|
|
|
+ def set_min_fee(self):
|
|
|
+ self.min_fee = self.sum_inputs() - self.sum_outputs() + self.get_relay_fee()
|
|
|
+
|
|
|
+ def get_usr_fee(self,tx_fee,desc):
|
|
|
+ ret = super(type(self),self).get_usr_fee(tx_fee,desc)
|
|
|
+ if ret < self.min_fee:
|
|
|
+ msg('{} BTC: {} fee too small. Minimum fee: {} BTC ({} satoshis per byte)'.format(
|
|
|
+ ret,desc,self.min_fee,self.btc2spb(self.min_fee)))
|
|
|
+ return False
|
|
|
+ output_amt = self.outputs[self.bump_output_idx].amt
|
|
|
+ if ret >= output_amt:
|
|
|
+ msg('{} BTC: {} fee too large. Maximum fee: <{} BTC'.format(ret,desc,output_amt))
|
|
|
+ return False
|
|
|
+ return ret
|