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 10 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