diff --git a/jmclient/jmclient/__init__.py b/jmclient/jmclient/__init__.py index be7c9fb..16d512b 100644 --- a/jmclient/jmclient/__init__.py +++ b/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. diff --git a/jmclient/jmclient/tumble_support.py b/jmclient/jmclient/taker_utils.py similarity index 70% rename from jmclient/jmclient/tumble_support.py rename to jmclient/jmclient/taker_utils.py index e25389b..b1eaac4 100644 --- a/jmclient/jmclient/tumble_support.py +++ b/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) diff --git a/scripts/joinmarket-qt.py b/scripts/joinmarket-qt.py index dc0a35b..a898090 100644 --- a/scripts/joinmarket-qt.py +++ b/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 + '
' 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.", diff --git a/scripts/sendpayment.py b/scripts/sendpayment.py index 5e44563..ee71ab5 100644 --- a/scripts/sendpayment.py +++ b/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")