Skip to content
Permalink
Browse files

lpos functions

  • Loading branch information...
mattt21 committed Feb 9, 2019
1 parent bcc86fd commit bcd774db2a3a1252739388be16f33ddd0df7537c
Binary file not shown.
@@ -263,6 +263,82 @@ def sufficient_funds(buckets):

return tx

def make_lpos_tx(self, coins, inputs, outputs, lpos_output, change_addrs, fee_estimator,
dust_threshold):
"""Select unspent coins to spend to pay outputs. If the change is
greater than dust_threshold (after adding the change output to
the transaction) it is kept, otherwise none is sent and it is
added to the transaction fee.
Note: fee_estimator expects virtual bytes
"""

# Deterministic randomness from coins
utxos = [c['prevout_hash'] + str(c['prevout_n']) for c in coins]
self.p = PRNG(''.join(sorted(utxos)))

# Copy the outputs so when adding change we don't modify "outputs"
tx = Transaction.from_lpos_io(inputs[:], outputs[:], lpos_output[:])
input_value = tx.input_value()

# Weight of the transaction with no inputs and no change
# Note: this will use legacy tx serialization as the need for "segwit"
# would be detected from inputs. The only side effect should be that the
# marker and flag are excluded, which is compensated in get_tx_weight()
# FIXME calculation will be off by this (2 wu) in case of RBF batching
base_weight = tx.estimated_weight()
spent_amount = tx.output_value()

def fee_estimator_w(weight):
return fee_estimator(Transaction.virtual_size_from_weight(weight))

def get_tx_weight(buckets):
total_weight = base_weight + sum(bucket.weight for bucket in buckets)
is_segwit_tx = any(bucket.witness for bucket in buckets)
if is_segwit_tx:
total_weight += 2 # marker and flag
# non-segwit inputs were previously assumed to have
# a witness of '' instead of '00' (hex)
# note that mixed legacy/segwit buckets are already ok
num_legacy_inputs = sum((not bucket.witness) * len(bucket.coins)
for bucket in buckets)
total_weight += num_legacy_inputs

return total_weight

def sufficient_funds(buckets):
'''Given a list of buckets, return True if it has enough
value to pay for the transaction'''
total_input = input_value + sum(bucket.value for bucket in buckets)
total_weight = get_tx_weight(buckets)
return total_input >= spent_amount + fee_estimator_w(total_weight)

# Collect the coins into buckets, choose a subset of the buckets
buckets = self.bucketize_coins(coins)
buckets = self.choose_buckets(buckets, sufficient_funds,
self.penalty_func(tx))

tx.add_inputs([coin for b in buckets for coin in b.coins])
tx_weight = get_tx_weight(buckets)

# change is sent back to sending address unless specified
if not change_addrs:
change_addrs = [tx.inputs()[0]['address']]
# note: this is not necessarily the final "first input address"
# because the inputs had not been sorted at this point
assert is_address(change_addrs[0])

# This takes a count of change outputs and returns a tx fee
output_weight = 4 * Transaction.estimated_output_size(change_addrs[0])
fee = lambda count: fee_estimator_w(tx_weight + count * output_weight)
change = self.change_outputs(tx, change_addrs, fee, dust_threshold)
tx.add_outputs(change)

self.print_error("using %d inputs" % len(tx.inputs()))
self.print_error("using buckets:", [bucket.desc for bucket in buckets])

return tx

def choose_buckets(self, buckets, sufficient_funds, penalty_func):
raise NotImplemented('To be subclassed')

@@ -1544,6 +1544,11 @@ def read_send_tab(self):
self.show_warning(_("Invalid Lines found:") + "\n\n" + '\n'.join([ _("Line #") + str(x[0]+1) + ": " + x[1] for x in errors]))
return
outputs = self.payto_e.get_outputs(self.is_max)
lpos_output = None
# if address is empty, assume lpos contract is being made
if self.payto_e.payto_address == None:
outputs = self.lease_to_e.get_outputs(self.is_max)
lpos_output = self.cold_lease_to_e.get_outputs(self.is_max)

if self.payto_e.is_alias and self.payto_e.validated is False:
alias = self.payto_e.toPlainText()
@@ -1557,6 +1562,19 @@ def read_send_tab(self):
self.show_error(_('No outputs'))
return


if self.payto_e.payto_address == None and self.lease_to_e.payto_address == None:
self.show_error(_('No recipients'))

if self.payto_e.payto_address != None and (self.lease_to_e.payto_address != None or self.cold_lease_to_e.payto_address != None):
self.show_error(_('Cannot make a regular transaction and LPoS contract at the same time'))

if self.cold_lease_to_e.payto_address != None and self.lease_to_e.payto_address == None:
self.show_error(_('No lease to address'))

if self.cold_lease_to_e.payto_address == None and self.lease_to_e.payto_address != None:
self.show_error(_('No fee reward address'))

for o in outputs:
if o.address is None:
self.show_error(_('NIX Address is None'))
@@ -1568,9 +1586,21 @@ def read_send_tab(self):
self.show_error(_('Invalid Amount'))
return

for o in lpos_output:
if o.address is None:
self.show_error(_('Fee Reward Address is None'))
return
if o.type == TYPE_ADDRESS and not bitcoin.is_address(o.address):
self.show_error(_('Invalid NIX Address'))
return
if o.value is None:
self.show_error(_('Invalid Amount'))
return


fee_estimator = self.get_send_fee_estimator()
coins = self.get_coins()
return outputs, fee_estimator, label, coins
return outputs, fee_estimator, label, coins, lpos_output

def do_preview(self):
self.do_send(preview = True)
@@ -1581,12 +1611,17 @@ def do_send(self, preview = False):
r = self.read_send_tab()
if not r:
return
outputs, fee_estimator, tx_desc, coins = r
outputs, fee_estimator, tx_desc, coins, lpos_output = r
try:
is_sweep = bool(self.tx_external_keypairs)
tx = self.wallet.make_unsigned_transaction(
coins, outputs, self.config, fixed_fee=fee_estimator,
is_sweep=is_sweep)
if not lpos_output:
tx = self.wallet.make_unsigned_transaction(
coins, outputs, self.config, fixed_fee=fee_estimator,
is_sweep=is_sweep)
else:
tx = self.wallet.make_unsigned_lpos_transaction(
coins, outputs, lpos_output, self.config, fixed_fee=fee_estimator,
is_sweep=is_sweep)
except (NotEnoughFunds, NoDynamicFeeEstimates) as e:
self.show_message(str(e))
return
@@ -795,6 +795,15 @@ def from_io(klass, inputs, outputs, locktime=0):
self.BIP69_sort()
return self

@classmethod
def from_lpos_io(klass, inputs, outputs, lpos_output, locktime=0):
self = klass(None)
self._inputs = inputs
self._outputs = outputs + lpos_output
self.locktime = locktime
self.BIP69_sort()
return self

@classmethod
def pay_script(self, output_type, addr: str) -> str:
"""Returns scriptPubKey in hex form."""
@@ -98,6 +98,29 @@ def append_utxos_to_inputs(inputs, network: 'Network', pubkey, txin_type, imax):
item['num_sig'] = 1
inputs.append(item)

def append_lpos_utxos_to_inputs(inputs, network: 'Network', pubkey, txin_type, imax):
if txin_type != 'p2pk':
address = bitcoin.pubkey_to_address(txin_type, pubkey)
scripthash = bitcoin.address_to_lpos_scripthash(address)
else:
script = bitcoin.public_key_to_p2pk_script(pubkey)
scripthash = bitcoin.script_to_scripthash(script)
address = '(pubkey)'

u = network.run_from_another_thread(network.listunspent_for_scripthash(scripthash))
for item in u:
if len(inputs) >= imax:
break
item['address'] = address
item['type'] = txin_type
item['prevout_hash'] = item['tx_hash']
item['prevout_n'] = int(item['tx_pos'])
item['pubkeys'] = [pubkey]
item['x_pubkeys'] = [pubkey]
item['signatures'] = [None]
item['num_sig'] = 1
inputs.append(item)

def sweep_preparations(privkeys, network: 'Network', imax=100):

def find_utxos_for_privkey(txin_type, privkey, compressed):
@@ -658,6 +681,96 @@ def fee_estimator(size: int) -> int:
run_hook('make_unsigned_transaction', self, tx)
return tx

def make_unsigned_lpos_transaction(self, coins, outputs, lpos_output, config, fixed_fee=None,
change_addr=None, is_sweep=False):
# check outputs
i_max = None
for i, o in enumerate(outputs):
if o.type == TYPE_ADDRESS:
if not is_address(o.address):
raise Exception("Invalid bitcoin address: {}".format(o.address))
if o.value == '!':
if i_max is not None:
raise Exception("More than one output set to spend max")
i_max = i

if fixed_fee is None and config.fee_per_kb() is None:
raise NoDynamicFeeEstimates()

for item in coins:
self.add_input_info(item)

# change address
# if we leave it empty, coin_chooser will set it
change_addrs = []
if change_addr:
change_addrs = [change_addr]
elif self.use_change:
# Recalc and get unused change addresses
addrs = self.calc_unused_change_addresses()
# New change addresses are created only after a few
# confirmations.
if addrs:
# if there are any unused, select all
change_addrs = addrs
else:
# if there are none, take one randomly from the last few
addrs = self.get_change_addresses()[-self.gap_limit_for_change:]
change_addrs = [random.choice(addrs)] if addrs else []

# Fee estimator
if fixed_fee is None:
fee_estimator = config.estimate_fee
elif isinstance(fixed_fee, Number):
fee_estimator = lambda size: fixed_fee
elif callable(fixed_fee):
fee_estimator = fixed_fee
else:
raise Exception('Invalid argument fixed_fee: %s' % fixed_fee)

if i_max is None:
# Let the coin chooser select the coins to spend
max_change = self.max_change_outputs if self.multiple_change else 1
coin_chooser = coinchooser.get_coin_chooser(config)
# If there is an unconfirmed RBF tx, merge with it
base_tx = self.get_unconfirmed_base_tx_for_batching()
if config.get('batch_rbf', False) and base_tx:
is_local = self.get_tx_height(base_tx.txid()).height == TX_HEIGHT_LOCAL
base_tx = Transaction(base_tx.serialize())
base_tx.deserialize(force_full_parse=True)
base_tx.remove_signatures()
base_tx.add_inputs_info(self)
base_tx_fee = base_tx.get_fee()
relayfeerate = self.relayfee() / 1000
original_fee_estimator = fee_estimator
def fee_estimator(size: int) -> int:
lower_bound = base_tx_fee + round(size * relayfeerate)
lower_bound = lower_bound if not is_local else 0
return max(lower_bound, original_fee_estimator(size))
txi = base_tx.inputs()
txo = list(filter(lambda o: not self.is_change(o.address), base_tx.outputs()))
else:
txi = []
txo = []
tx = coin_chooser.make_lpos_tx(coins, txi, outputs[:] + txo, lpos_output[:], change_addrs[:max_change],
fee_estimator, self.dust_threshold())
else:
# FIXME?? this might spend inputs with negative effective value...
sendable = sum(map(lambda x:x['value'], coins))
outputs[i_max] = outputs[i_max]._replace(value=0)
tx = Transaction.from_lpos_io(coins, outputs[:], lpos_output[:])
fee = fee_estimator(tx.estimated_size())
amount = sendable - tx.output_value() - fee
if amount < 0:
raise NotEnoughFunds()
outputs[i_max] = outputs[i_max]._replace(value=amount)
tx = Transaction.from_lpos_io(coins, outputs[:])

# Timelock tx to current height.
tx.locktime = self.get_local_height()
run_hook('make_unsigned_transaction', self, tx)
return tx

def mktx(self, outputs, password, config, fee=None, change_addr=None,
domain=None, rbf=False, nonlocal_only=False):
coins = self.get_spendable_coins(domain, config, nonlocal_only=nonlocal_only)
@@ -666,6 +779,14 @@ def mktx(self, outputs, password, config, fee=None, change_addr=None,
self.sign_transaction(tx, password)
return tx

def mklpostx(self, outputs, lpos_output, password, config, fee=None, change_addr=None,
domain=None, rbf=False, nonlocal_only=False):
coins = self.get_spendable_coins(domain, config, nonlocal_only=nonlocal_only)
tx = self.make_unsigned_lpos_transaction(coins, outputs, lpos_output, config, fee, change_addr)
tx.set_rbf(rbf)
self.sign_transaction(tx, password)
return tx

def is_frozen(self, addr):
return addr in self.frozen_addresses

0 comments on commit bcd774d

Please sign in to comment.
You can’t perform that action at this time.