Browse Source

Support direct send in Qt

via 0 entry in num counterparties. Modify tooltip to reflect.
Rename tumble_support to taker_utils for generic Taker code.
Refactor direct_send function to that module, shared with
sendpayment CLI script.
master
Adam Gibson 9 years ago
parent
commit
3a43869ddf
No known key found for this signature in database
GPG Key ID: B3AE09F1E9A3197A
  1. 4
      jmclient/jmclient/__init__.py
  2. 87
      jmclient/jmclient/taker_utils.py
  3. 42
      scripts/joinmarket-qt.py
  4. 61
      scripts/sendpayment.py

4
jmclient/jmclient/__init__.py

@ -34,8 +34,8 @@ from .schedule import (get_schedule, get_tumble_schedule, schedule_to_text,
tweak_tumble_schedule, human_readable_schedule_entry,
schedule_to_text)
from .commitment_utils import get_utxo_info, validate_utxo_data, quit
from .tumble_support import (tumbler_taker_finished_update, restart_waiter,
restart_wait, get_tumble_log,
from .taker_utils import (tumbler_taker_finished_update, restart_waiter,
restart_wait, get_tumble_log, direct_send,
tumbler_filter_orders_callback)
# Set default logging handler to avoid "No handler found" warnings.

87
jmclient/jmclient/tumble_support.py → jmclient/jmclient/taker_utils.py

@ -6,7 +6,8 @@ import os
import time
from .configure import get_log, jm_single, validate_address
from .schedule import human_readable_schedule_entry, tweak_tumble_schedule
from .wallet import Wallet, estimate_tx_fee
from jmclient import mktx, deserialize, sign, txhash
log = get_log()
"""
@ -14,6 +15,90 @@ Utility functions for tumbler-style takers;
Currently re-used by CLI script tumbler.py and joinmarket-qt
"""
def direct_send(wallet, amount, mixdepth, destaddr, answeryes=False,
accept_callback=None, info_callback=None):
"""Send coins directly from one mixdepth to one destination address;
does not need IRC. Sweep as for normal sendpayment (set amount=0).
If answeryes is True, callback/command line query is not performed.
If accept_callback is None, command line input for acceptance is assumed,
else this callback is called:
accept_callback:
====
args:
deserialized tx, destination address, amount in satoshis, fee in satoshis
returns:
True if accepted, False if not
====
The info_callback takes one parameter, the information message (when tx is
pushed), and returns nothing.
This function returns:
The txid if transaction is pushed, False otherwise
"""
#Sanity checks
assert validate_address(destaddr)[0]
assert isinstance(mixdepth, int)
assert mixdepth >= 0
assert isinstance(amount, int)
assert amount >=0
assert isinstance(wallet, Wallet)
from pprint import pformat
if amount == 0:
utxos = wallet.get_utxos_by_mixdepth()[mixdepth]
if utxos == {}:
log.error(
"There are no utxos in mixdepth: " + str(mixdepth) + ", quitting.")
return
total_inputs_val = sum([va['value'] for u, va in utxos.iteritems()])
fee_est = estimate_tx_fee(len(utxos), 1)
outs = [{"address": destaddr, "value": total_inputs_val - fee_est}]
else:
initial_fee_est = estimate_tx_fee(8,2) #8 inputs to be conservative
utxos = wallet.select_utxos(mixdepth, amount + initial_fee_est)
if len(utxos) < 8:
fee_est = estimate_tx_fee(len(utxos), 2)
else:
fee_est = initial_fee_est
total_inputs_val = sum([va['value'] for u, va in utxos.iteritems()])
changeval = total_inputs_val - fee_est - amount
outs = [{"value": amount, "address": destaddr}]
change_addr = wallet.get_internal_addr(mixdepth)
outs.append({"value": changeval, "address": change_addr})
#Now ready to construct transaction
log.info("Using a fee of : " + str(fee_est) + " satoshis.")
if amount != 0:
log.info("Using a change value of: " + str(changeval) + " satoshis.")
tx = mktx(utxos.keys(), outs)
stx = deserialize(tx)
for index, ins in enumerate(stx['ins']):
utxo = ins['outpoint']['hash'] + ':' + str(
ins['outpoint']['index'])
addr = utxos[utxo]['address']
tx = sign(tx, index, wallet.get_key_from_addr(addr))
txsigned = deserialize(tx)
log.info("Got signed transaction:\n")
log.info(tx + "\n")
log.info(pformat(txsigned))
if not answeryes:
if not accept_callback:
if raw_input('Would you like to push to the network? (y/n):')[0] != 'y':
log.info("You chose not to broadcast the transaction, quitting.")
return False
else:
actual_amount = amount if amount != 0 else total_inputs_val - fee_est
accepted = accept_callback(pformat(txsigned), destaddr, actual_amount,
fee_est)
if not accepted:
return False
jm_single().bc_interface.pushtx(tx)
txid = txhash(tx)
successmsg = "Transaction sent: " + txid
cb = log.info if not info_callback else info_callback
cb(successmsg)
return txid
def get_tumble_log(logsdir):
tumble_log = logging.getLogger('tumbler')
tumble_log.setLevel(logging.DEBUG)

42
scripts/joinmarket-qt.py

@ -50,7 +50,7 @@ from jmclient import (load_program_config, get_network, Wallet,
JMTakerClientProtocolFactory, WalletError,
start_reactor, get_schedule, get_tumble_schedule,
schedule_to_text, mn_decode, mn_encode, create_wallet_file,
get_blockchain_interface_instance, sync_wallet,
get_blockchain_interface_instance, sync_wallet, direct_send,
RegtestBitcoinCoreInterface, tweak_tumble_schedule,
human_readable_schedule_entry, tumbler_taker_finished_update,
get_tumble_log, restart_wait, tumbler_filter_orders_callback)
@ -111,7 +111,8 @@ def getSettingsWidgets():
'Amount in bitcoins (BTC)']
sH = ['The address you want to send the payment to',
'How many other parties to send to; if you enter 4\n' +
', there will be 5 participants, including you',
', there will be 5 participants, including you.\n' +
'Enter 0 to send direct without coinjoin.',
'The mixdepth of the wallet to send the payment from',
'The amount IN BITCOINS to send.\n' +
'If you enter 0, a SWEEP transaction\nwill be performed,' +
@ -581,6 +582,25 @@ class SpendTab(QWidget):
self.updateSchedView()
self.startJoin()
def checkDirectSend(self, dtx, destaddr, amount, fee):
"""Give user info to decide whether to accept a direct send;
note the callback includes the full prettified transaction,
but currently not printing it for space reasons.
"""
mbinfo = ["Sending " + satoshis_to_amt_str(amount) + ",",
"to: " + destaddr + ",",
"Fee: " + satoshis_to_amt_str(fee) + ".",
"Accept?"]
reply = JMQtMessageBox(self, '\n'.join([m + '<p>' for m in mbinfo]),
mbtype='question', title="Direct send")
if reply == QMessageBox.Yes:
return True
else:
return False
def infoDirectSend(self, txid):
JMQtMessageBox(self, "Tx sent: " + str(txid), title="Success")
def startSingle(self):
if not self.spendstate.runstate == 'ready':
log.info("Cannot start join, already running.")
@ -592,6 +612,17 @@ class SpendTab(QWidget):
amount = int(Decimal(btc_amount_str) * Decimal('1e8'))
makercount = int(self.widgets[1][1].text())
mixdepth = int(self.widgets[2][1].text())
if makercount == 0:
txid = direct_send(w.wallet, amount, mixdepth,
destaddr, accept_callback=self.checkDirectSend,
info_callback=self.infoDirectSend)
if not txid:
self.giveUp()
else:
self.persistTxToHistory(destaddr, amount, txid)
self.cleanUp()
return
#note 'amount' is integer, so not interpreted as fraction
#see notes in sample testnet schedule for format
self.spendstate.loaded_schedule = [[mixdepth, amount, makercount,
@ -900,7 +931,7 @@ class SpendTab(QWidget):
"""
log.debug("Transaction aborted.")
w.statusBar().showMessage("Transaction aborted.")
if len(self.taker.ignored_makers) > 0:
if self.taker and len(self.taker.ignored_makers) > 0:
JMQtMessageBox(self, "These Makers did not respond, and will be \n"
"ignored in future: \n" + str(
','.join(self.taker.ignored_makers)),
@ -927,11 +958,6 @@ class SpendTab(QWidget):
if self.widgets[i][1].text().size() == 0:
JMQtMessageBox(self, errs[i - 1], mbtype='warn', title="Error")
return False
#QIntValidator does not prevent entry of 0 for counterparties.
#Note, use of '1' is not recommended, but not prevented here.
if self.widgets[1][1].text() == '0':
JMQtMessageBox(self, errs[0], mbtype='warn', title="Error")
return False
if not w.wallet:
JMQtMessageBox(self,
"There is no wallet loaded.",

61
scripts/sendpayment.py

@ -24,7 +24,7 @@ from jmclient import (Taker, load_program_config, get_schedule,
cheapest_order_choose, weighted_order_choose,
Wallet, BitcoinCoreWallet, sync_wallet,
RegtestBitcoinCoreInterface, estimate_tx_fee,
mktx, deserialize, sign, txhash)
direct_send)
from jmbase.support import get_log, debug_dump_object, get_password
from cli_options import get_sendpayment_parser
@ -53,63 +53,6 @@ def pick_order(orders, n): #pragma: no cover
return orders[pickedOrderIndex]
pickedOrderIndex = -1
def direct_send(wallet, amount, mixdepth, destaddr, answeryes=False):
"""Send coins directly from one mixdepth to one destination address;
does not need IRC. Sweep as for normal sendpayment (set amount=0).
"""
#Sanity checks; note destaddr format is carefully checked in startup
assert isinstance(mixdepth, int)
assert mixdepth >= 0
assert isinstance(amount, int)
assert amount >=0 and amount < 10000000000
assert isinstance(wallet, Wallet)
from pprint import pformat
if amount == 0:
utxos = wallet.get_utxos_by_mixdepth()[mixdepth]
if utxos == {}:
log.error(
"There are no utxos in mixdepth: " + str(mixdepth) + ", quitting.")
return
total_inputs_val = sum([va['value'] for u, va in utxos.iteritems()])
fee_est = estimate_tx_fee(len(utxos), 1)
outs = [{"address": destaddr, "value": total_inputs_val - fee_est}]
else:
initial_fee_est = estimate_tx_fee(8,2) #8 inputs to be conservative
utxos = wallet.select_utxos(mixdepth, amount + initial_fee_est)
if len(utxos) < 8:
fee_est = estimate_tx_fee(len(utxos), 2)
else:
fee_est = initial_fee_est
total_inputs_val = sum([va['value'] for u, va in utxos.iteritems()])
changeval = total_inputs_val - fee_est - amount
outs = [{"value": amount, "address": destaddr}]
change_addr = wallet.get_internal_addr(mixdepth)
outs.append({"value": changeval, "address": change_addr})
#Now ready to construct transaction
log.info("Using a fee of : " + str(fee_est) + " satoshis.")
if amount != 0:
log.info("Using a change value of: " + str(changeval) + " satoshis.")
tx = mktx(utxos.keys(), outs)
stx = deserialize(tx)
for index, ins in enumerate(stx['ins']):
utxo = ins['outpoint']['hash'] + ':' + str(
ins['outpoint']['index'])
addr = utxos[utxo]['address']
tx = sign(tx, index, wallet.get_key_from_addr(addr))
txsigned = deserialize(tx)
log.info("Got signed transaction:\n")
log.info(tx + "\n")
log.info(pformat(txsigned))
if not answeryes:
if raw_input('Would you like to push to the network? (y/n):')[0] != 'y':
log.info("You chose not to broadcast the transaction, quitting.")
return
jm_single().bc_interface.pushtx(tx)
txid = txhash(tx)
log.info("Transaction sent: " + txid + ", shutting down")
def main():
parser = get_sendpayment_parser()
(options, args) = parser.parse_args()
@ -198,8 +141,6 @@ def main():
wallet = BitcoinCoreWallet(fromaccount=wallet_name)
sync_wallet(wallet, fast=options.fastsync)
#Note that direct send is currently only supported for command line,
#not for schedule file (in that case options.makercount is 4-6, not 0)
if options.makercount == 0:
if isinstance(wallet, BitcoinCoreWallet):
raise NotImplementedError("Direct send only supported for JM wallets")

Loading…
Cancel
Save