Browse Source

Enable external/custom change addresses.

Fixes #797.

Adds `custom_change_addr` argument to `direct_send()`
joinmarket-qt: Adds input field for optional external change address
joinmarket-qt: Better handle PayJoin/CoinJoin state changes for changeInput widget
Adds `custom_change_address` argument to Taker constructor and use it in joinmarket-qt
Custom change also allowed in sendpayment CLI with `-u` flag (not
supported in tumbler).
Explicitly disallows using custom change with BIP78 Payjoin, though that
could change later.
Both sendpayment and CLI provide detailed warnings to avoid misuse. In
particular, they have an extra warning for using a nonstandard or
non-wallet scriptpubkey type.
Setting custom change to the recipient address is explicitly forbidden.

Tests: Adds custom_change usage test in test_taker.
master
csH7KmCC9 5 years ago committed by Adam Gibson
parent
commit
ad8cd74ee9
No known key found for this signature in database
GPG Key ID: 141001A1AF77F20B
  1. 5
      jmclient/jmclient/__init__.py
  2. 7
      jmclient/jmclient/cli_options.py
  3. 31
      jmclient/jmclient/output.py
  4. 15
      jmclient/jmclient/taker.py
  5. 24
      jmclient/jmclient/taker_utils.py
  6. 3
      jmclient/test/commontest.py
  7. 45
      jmclient/test/test_taker.py
  8. 112
      scripts/joinmarket-qt.py
  9. 46
      scripts/sendpayment.py

5
jmclient/jmclient/__init__.py

@ -19,7 +19,7 @@ from .wallet import (Mnemonic, estimate_tx_fee, WalletError, BaseWallet, ImportW
from .storage import (Argon2Hash, Storage, StorageError, RetryableStorageError,
StoragePasswordError, VolatileStorage)
from .cryptoengine import (BTCEngine, BTC_P2PKH, BTC_P2SH_P2WPKH, BTC_P2WPKH, EngineError,
TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH)
TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, detect_script_type)
from .configure import (load_test_config, process_shutdown,
load_program_config, jm_single, get_network, update_persist_config,
validate_address, is_burn_destination, get_irc_mchannels,
@ -36,7 +36,8 @@ from .podle import (set_commitment_file, get_commitment_file,
PoDLE, generate_podle, get_podle_commitments,
update_commitments)
from .output import generate_podle_error_string, fmt_utxos, fmt_utxo,\
fmt_tx_data
fmt_tx_data, general_custom_change_warning, nonwallet_custom_change_warning,\
sweep_custom_change_warning
from .schedule import (get_schedule, get_tumble_schedule, schedule_to_text,
tweak_tumble_schedule, human_readable_schedule_entry,
schedule_to_text, NO_ROUNDING)

7
jmclient/jmclient/cli_options.py

@ -519,6 +519,13 @@ def get_sendpayment_parser():
'broadcasting the transaction. '
'Currently only works with direct '
'send (-N 0).')
parser.add_option('-u',
'--custom-change',
type="str",
dest='customchange',
default='',
help='specify address to receive change '
', by default use in-wallet address.')
add_common_options(parser)
return parser

31
jmclient/jmclient/output.py

@ -1,5 +1,36 @@
from jmbase import utxo_to_utxostr
general_custom_change_warning = """You are attempting to send change to a custom change
address. Change outputs are usually directly linkable to
your CoinJoin inputs, and incautious combination of
custom change UTXOs can catastrophically compromise
your CoinJoin privacy, especially if those UTXOs are from
different mixdepths.
Are you sure you know what you're doing?"""
nonwallet_custom_change_warning ="""
The custom change address type is different from your wallet
address type.
Be extremely careful here: It will be obvious to any blockchain
observer that this output was disposed of by the taker (i.e.
you) and is directly linkable to your CoinJoin inputs.
Sending change in a one-off transaction to a party with a
different address type than this wallet is otherwise probably
OK.
HOWEVER, if you regularly send your change to unusual
address types, especially multisig P2(W)SH addresses, you
seriously risk linking ALL of those CoinJoins to you,
REGARDLESS of how carefully you spend those custom change
UTXOs.
Are you sure you want to continue?"""
sweep_custom_change_warning = \
"Custom change cannot be set while doing a sweep (zero amount)."
def fmt_utxos(utxos, wallet_service, prefix=''):
output = []

15
jmclient/jmclient/taker.py

@ -33,6 +33,7 @@ class Taker(object):
order_chooser=weighted_order_choose,
callbacks=None,
tdestaddrs=None,
custom_change_address=None,
ignored_makers=None):
"""`schedule`` must be a list of tuples: (see sample_schedule_for_testnet
for explanation of syntax, also schedule.py module in this directory),
@ -84,6 +85,7 @@ class Taker(object):
self.schedule = schedule
self.order_chooser = order_chooser
self.max_cj_fee = max_cj_fee
self.my_change_addr = custom_change_address
#List (which persists between transactions) of makers
#who have not responded or behaved maliciously at any
@ -288,13 +290,13 @@ class Taker(object):
if not self.my_cj_addr:
#previously used for donations; TODO reimplement?
raise NotImplementedError
self.my_change_addr = None
if self.cjamount != 0:
try:
self.my_change_addr = self.wallet_service.get_internal_addr(self.mixdepth)
except:
self.taker_info_callback("ABORT", "Failed to get a change address")
return False
if self.my_change_addr is None:
try:
self.my_change_addr = self.wallet_service.get_internal_addr(self.mixdepth)
except:
self.taker_info_callback("ABORT", "Failed to get a change address")
return False
#adjust the required amount upwards to anticipate an increase in
#transaction fees after re-estimation; this is sufficiently conservative
#to make failures unlikely while keeping the occurence of failure to
@ -314,6 +316,7 @@ class Taker(object):
else:
#sweep
self.input_utxos = self.wallet_service.get_utxos_by_mixdepth()[self.mixdepth]
self.my_change_addr = None
#do our best to estimate the fee based on the number of
#our own utxos; this estimate may be significantly higher
#than the default set in option.txfee * makercount, where

24
jmclient/jmclient/taker_utils.py

@ -24,7 +24,7 @@ Currently re-used by CLI script tumbler.py and joinmarket-qt
def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False,
accept_callback=None, info_callback=None, error_callback=None,
return_transaction=False, with_final_psbt=False,
optin_rbf=False):
optin_rbf=False, custom_change_addr=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.
@ -34,7 +34,9 @@ def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False,
accept_callback:
====
args:
deserialized tx, destination address, amount in satoshis, fee in satoshis
deserialized tx, destination address, amount in satoshis,
fee in satoshis, custom change address
returns:
True if accepted, False if not
====
@ -52,6 +54,8 @@ def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False,
"""
#Sanity checks
assert validate_address(destination)[0] or is_burn_destination(destination)
assert custom_change_addr is None or validate_address(custom_change_addr)[0]
assert amount > 0 or custom_change_addr is None
assert isinstance(mixdepth, numbers.Integral)
assert mixdepth >= 0
assert isinstance(amount, numbers.Integral)
@ -77,6 +81,7 @@ def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False,
txtype = wallet_service.get_txtype()
if amount == 0:
#doing a sweep
utxos = wallet_service.get_utxos_by_mixdepth()[mixdepth]
if utxos == {}:
log.error(
@ -105,10 +110,11 @@ def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False,
+ wallet_service.wallet.get_path_repr(path) \
+ "\n\nWARNING: This transaction if broadcasted will PERMANENTLY DESTROY your bitcoins\n"
else:
#regular send (non-burn)
#regular sweep (non-burn)
fee_est = estimate_tx_fee(len(utxos), 1, txtype=txtype)
outs = [{"address": destination, "value": total_inputs_val - fee_est}]
else:
#not doing a sweep; we will have change
#8 inputs to be conservative
initial_fee_est = estimate_tx_fee(8,2, txtype=txtype)
utxos = wallet_service.select_utxos(mixdepth, amount + initial_fee_est)
@ -119,7 +125,8 @@ def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False,
total_inputs_val = sum([va['value'] for u, va in utxos.items()])
changeval = total_inputs_val - fee_est - amount
outs = [{"value": amount, "address": destination}]
change_addr = wallet_service.get_internal_addr(mixdepth)
change_addr = wallet_service.get_internal_addr(mixdepth) if custom_change_addr is None \
else custom_change_addr
outs.append({"value": changeval, "address": change_addr})
#compute transaction locktime, has special case for spending timelocked coins
@ -175,7 +182,11 @@ def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False,
log.info("Got signed transaction:\n")
log.info(human_readable_transaction(tx))
actual_amount = amount if amount != 0 else total_inputs_val - fee_est
log.info("Sends: " + amount_to_str(actual_amount) + " to destination: " + destination)
sending_info = "Sends: " + amount_to_str(actual_amount) + \
" to destination: " + destination
if custom_change_addr:
sending_info += ", custom change to: " + custom_change_addr
log.info(sending_info)
if not answeryes:
if not accept_callback:
if input('Would you like to push to the network? (y/n):')[0] != 'y':
@ -183,7 +194,8 @@ def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False,
return False
else:
accepted = accept_callback(human_readable_transaction(tx),
destination, actual_amount, fee_est)
destination, actual_amount, fee_est,
custom_change_addr)
if not accepted:
return False
if jm_single().bc_interface.pushtx(tx.serialize()):

3
jmclient/test/commontest.py

@ -26,7 +26,8 @@ PINL = '\r\n' if OS == 'Windows' else '\n'
default_max_cj_fee = (1, float('inf'))
# callbacks for making transfers in-script with direct_send:
def dummy_accept_callback(tx, destaddr, actual_amount, fee_est):
def dummy_accept_callback(tx, destaddr, actual_amount, fee_est,
custom_change_addr):
return True
def dummy_info_callback(msg):
pass

45
jmclient/test/test_taker.py

@ -13,8 +13,8 @@ from base64 import b64encode
from jmbase import utxostr_to_utxo, hextobin
from jmclient import load_test_config, jm_single, set_commitment_file,\
get_commitment_file, SegwitWallet, Taker, VolatileStorage,\
get_network, WalletService, NO_ROUNDING, BTC_P2PKH,\
NotEnoughFundsException
get_network, WalletService, NO_ROUNDING, NotEnoughFundsException,\
BTC_P2SH_P2WPKH, BTC_P2PKH, BTC_P2WPKH
from taker_test_data import t_utxos_by_mixdepth, t_orderbook,\
t_maker_response, t_chosen_orders, t_dummy_ext
from commontest import default_max_cj_fee
@ -146,7 +146,7 @@ def dummy_filter_orderbook(orders_fees, cjamount):
return True
def get_taker(schedule=None, schedule_len=0, on_finished=None,
filter_orders=None):
filter_orders=None, custom_change=None):
if not schedule:
#note, for taker.initalize() this will result in junk
schedule = [['a', 'b', 'c', 'd', 'e', 'f']]*schedule_len
@ -154,7 +154,8 @@ def get_taker(schedule=None, schedule_len=0, on_finished=None,
on_finished_callback = on_finished if on_finished else taker_finished
filter_orders_callback = filter_orders if filter_orders else dummy_filter_orderbook
taker = Taker(WalletService(DummyWallet()), schedule, default_max_cj_fee,
callbacks=[filter_orders_callback, None, on_finished_callback])
callbacks=[filter_orders_callback, None, on_finished_callback],
custom_change_address=custom_change)
taker.wallet_service.current_blockheight = 10**6
return taker
@ -438,10 +439,46 @@ def test_taker_init(setup_taker, schedule, highfee, toomuchcoins, minmakers,
a = taker.coinjoin_address()
taker.wallet_service.wallet.inject_addr_get_failure = True
taker.my_cj_addr = "dummy"
taker.my_change_addr = None
assert not taker.prepare_my_bitcoin_data()
#clean up
return clean_up()
def test_custom_change(setup_taker):
# create three random custom change addresses, one of each
# known type in Joinmarket.
privs = [x*32 + b"\x01" for x in [struct.pack(b'B', y) for y in range(1,4)]]
scripts = [a.key_to_script(i) for a, i in zip([BTC_P2PKH, BTC_P2SH_P2WPKH, BTC_P2WPKH], privs)]
addrs = [a.privkey_to_address(i) for a, i in zip([BTC_P2PKH, BTC_P2SH_P2WPKH, BTC_P2WPKH], privs)]
schedule = [(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)]
for script, addr in zip(scripts, addrs):
taker = get_taker(schedule, custom_change=addr)
orderbook = copy.deepcopy(t_orderbook)
res = taker.initialize(orderbook)
taker.orderbook = copy.deepcopy(t_chosen_orders)
maker_response = copy.deepcopy(t_maker_response)
res = taker.receive_utxos(maker_response)
assert res[0]
# ensure that the transaction created for signing has
# the address we intended with the right amount:
custom_change_found = False
for out in taker.latest_tx.vout:
# input utxo is 20M; amount is 2M; as per logs:
# totalin=200000000
# my_txfee=12930
# makers_txfee=3000
# cjfee_total=12000 => changevalue=179975070
# note that there is a small variation in the size of
# the transaction (a few bytes) for the different scriptPubKey
# type, but this is currently ignored by the Taker, who makes
# fee estimate purely based on the number of ins and outs;
# this will never be too far off anyway.
if out.scriptPubKey == script and out.nValue == 179975070:
# must be only one
assert not custom_change_found
custom_change_found = True
assert custom_change_found
@pytest.mark.parametrize(
"schedule_len",
[

112
scripts/joinmarket-qt.py

@ -70,7 +70,10 @@ from jmclient import load_program_config, get_network, update_persist_config,\
NO_ROUNDING, get_max_cj_fee_values, get_default_max_absolute_fee, \
get_default_max_relative_fee, RetryableStorageError, add_base_options, \
BTCEngine, FidelityBondMixin, wallet_change_passphrase, \
parse_payjoin_setup, send_payjoin, JMBIP78ReceiverManager
parse_payjoin_setup, send_payjoin, JMBIP78ReceiverManager, \
detect_script_type, general_custom_change_warning, \
nonwallet_custom_change_warning, sweep_custom_change_warning, EngineError
from qtsupport import ScheduleWizard, TumbleRestartWizard, config_tips,\
config_types, QtHandler, XStream, Buttons, OkButton, CancelButton,\
PasswordDialog, MyTreeWidget, JMQtMessageBox, BLUE_FG,\
@ -298,9 +301,11 @@ class SpendTab(QWidget):
self.pjEndpointLabel.setVisible(True)
self.pjEndpointInput.setVisible(True)
# while user is attempting a payjoin, address
# while user is attempting a payjoin, address/change
# cannot be edited; to back out, they hit Abort.
self.addressInput.setEnabled(False)
self.changeInput.setEnabled(False)
self.changeInput.clear()
self.abortButton.setEnabled(True)
def switchToJoinmarket(self):
@ -314,14 +319,24 @@ class SpendTab(QWidget):
self.switchToJoinmarket()
self.addressInput.setText('')
self.amountInput.setText('')
self.changeInput.setText('')
self.addressInput.setEnabled(True)
self.pjEndpointInput.setEnabled(True)
self.mixdepthInput.setEnabled(True)
self.amountInput.setEnabled(True)
self.changeInput.setEnabled(True)
self.startButton.setEnabled(True)
self.abortButton.setEnabled(False)
def checkAddress(self, addr):
valid, errmsg = validate_address(str(addr))
if not valid and len(addr) > 0:
JMQtMessageBox(self,
"Bitcoin address not valid.\n" + errmsg,
mbtype='warn',
title="Error")
def parseURIAndValidateAddress(self, addr):
addr = addr.strip()
if btc.is_bip21_uri(addr):
try:
@ -344,12 +359,7 @@ class SpendTab(QWidget):
self.bip21_uri = None
self.addressInput.setText(addr)
valid, errmsg = validate_address(str(addr))
if not valid and len(addr) > 0:
JMQtMessageBox(self,
"Bitcoin address not valid.\n" + errmsg,
mbtype='warn',
title="Error")
self.checkAddress(addr)
def checkAmount(self, amount_str):
if not amount_str:
@ -519,6 +529,7 @@ class SpendTab(QWidget):
sch_buttons_box.setLayout(sch_buttons_layout)
sch_layout.addWidget(sch_buttons_box, 0, 1, 1, 1)
#construct layout for single joins
innerTopLayout = QGridLayout()
innerTopLayout.setSpacing(4)
self.single_join_tab.setLayout(innerTopLayout)
@ -531,7 +542,7 @@ class SpendTab(QWidget):
'The address or bitcoin: URI you want to send the payment to')
self.addressInput = QLineEdit()
self.addressInput.editingFinished.connect(
lambda: self.checkAddress(self.addressInput.text()))
lambda: self.parseURIAndValidateAddress(self.addressInput.text()))
innerTopLayout.addWidget(recipientLabel, 1, 0)
innerTopLayout.addWidget(self.addressInput, 1, 1, 1, 2)
@ -570,6 +581,17 @@ class SpendTab(QWidget):
innerTopLayout.addWidget(amountLabel, 4, 0)
innerTopLayout.addWidget(self.amountInput, 4, 1, 1, 2)
changeLabel = QLabel('Custom change address')
changeLabel.setToolTip(
'Specify an address to receive change, rather ' +
'than sending it to the internal wallet.')
self.changeInput = QLineEdit()
self.changeInput.editingFinished.connect(
lambda: self.checkAddress(self.changeInput.text().strip()))
self.changeInput.setPlaceholderText("(optional)")
innerTopLayout.addWidget(changeLabel, 5, 0)
innerTopLayout.addWidget(self.changeInput, 5, 1, 1, 2)
self.startButton = QPushButton('Start')
self.startButton.setToolTip(
'If "checktx" is selected in the Settings, you will be \n'
@ -585,7 +607,7 @@ class SpendTab(QWidget):
buttons.addWidget(self.startButton)
buttons.addWidget(self.abortButton)
self.abortButton.clicked.connect(self.abortTransactions)
innerTopLayout.addLayout(buttons, 5, 0, 1, 2)
innerTopLayout.addLayout(buttons, 6, 0, 1, 2)
splitter1 = QSplitter(QtCore.Qt.Vertical)
self.textedit = QTextEdit()
self.textedit.verticalScrollBar().rangeChanged.connect(
@ -676,14 +698,18 @@ class SpendTab(QWidget):
self.updateSchedView()
self.startJoin()
def checkDirectSend(self, dtx, destaddr, amount, fee):
def checkDirectSend(self, dtx, destaddr, amount, fee, custom_change_addr):
"""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 " + btc.amount_to_str(amount) + ",",
"to: " + destaddr + ",",
"Fee: " + btc.amount_to_str(fee) + ".",
"to: " + destaddr + ","]
if custom_change_addr:
mbinfo.append("change to: " + custom_change_addr + ",")
mbinfo += ["fee: " + btc.amount_to_str(fee) + ".",
"Accept?"]
reply = JMQtMessageBox(self, '\n'.join([m + '<p>' for m in mbinfo]),
mbtype='question', title="Direct send")
@ -716,11 +742,15 @@ class SpendTab(QWidget):
bip78url = self.pjEndpointInput.text()
if makercount == 0 and not bip78url:
custom_change = None
if len(self.changeInput.text().strip()) > 0:
custom_change = str(self.changeInput.text().strip())
try:
txid = direct_send(mainWindow.wallet_service, amount, mixdepth,
destaddr, accept_callback=self.checkDirectSend,
info_callback=self.infoDirectSend,
error_callback=self.errorDirectSend)
error_callback=self.errorDirectSend,
custom_change_addr=custom_change)
except Exception as e:
JMQtMessageBox(self, e.args[0], title="Error", mbtype="warn")
return
@ -761,6 +791,7 @@ class SpendTab(QWidget):
self.pjEndpointInput.setEnabled(False)
self.mixdepthInput.setEnabled(False)
self.amountInput.setEnabled(False)
self.changeInput.setEnabled(False)
self.startButton.setEnabled(False)
d = task.deferLater(reactor, 0.0, send_payjoin, manager,
accept_callback=self.checkDirectSend,
@ -826,6 +857,9 @@ class SpendTab(QWidget):
check_offers_callback = None
destaddrs = self.tumbler_destaddrs if self.tumbler_options else []
custom_change = None
if len(self.changeInput.text().strip()) > 0:
custom_change = str(self.changeInput.text().strip())
maxcjfee = get_max_cj_fee_values(jm_single().config, None,
user_callback=self.getMaxCJFees)
log.info("Using maximum coinjoin fee limits per maker of {:.4%}, {} "
@ -838,6 +872,7 @@ class SpendTab(QWidget):
self.takerInfo,
self.takerFinished],
tdestaddrs=destaddrs,
custom_change_address=custom_change,
ignored_makers=ignored_makers)
if not self.clientfactory:
#First run means we need to start: create clientfactory
@ -1117,6 +1152,55 @@ class SpendTab(QWidget):
"Amount, in bitcoins, must be provided.",
mbtype='warn', title="Error")
return False
if len(self.changeInput.text().strip()) != 0:
dest_addr = str(self.addressInput.text().strip())
change_addr = str(self.changeInput.text().strip())
makercount = int(self.numCPInput.text())
try:
amount = btc.amount_to_sat(self.amountInput.text())
except ValueError as e:
JMQtMessageBox(self, e.args[0], title="Error", mbtype="warn")
return False
valid, errmsg = validate_address(change_addr)
if not valid:
JMQtMessageBox(self,
"Custom change address is invalid: \"%s\"" % errmsg,
mbtype='warn', title="Error")
return False
if change_addr == dest_addr:
msg = ''.join(["Custom change address cannot be the ",
"same as the recipient address."])
JMQtMessageBox(self,
msg,
mbtype='warn', title="Error")
return False
if amount == 0:
JMQtMessageBox(self, sweep_custom_change_warning,
mbtype='warn', title="Error")
return False
if makercount > 0:
reply = JMQtMessageBox(self, general_custom_change_warning,
mbtype='question', title="Warning")
if reply == QMessageBox.No:
return False
change_spk = mainWindow.wallet_service.addr_to_script(change_addr)
engine_recognized = True
try:
change_addr_type = detect_script_type(change_spk)
except EngineError:
engine_recognized = False
wallet_type = mainWindow.wallet_service.TYPE
if (not engine_recognized) or (
change_addr_type != wallet_type and makercount > 0):
reply = JMQtMessageBox(self,
nonwallet_custom_change_warning,
mbtype='question',
title="Warning")
if reply == QMessageBox.No:
return False
return True
class TxHistoryTab(QWidget):

46
scripts/sendpayment.py

@ -16,7 +16,9 @@ from jmclient import Taker, load_program_config, get_schedule,\
jm_single, estimate_tx_fee, direct_send, WalletService,\
open_test_wallet_maybe, get_wallet_path, NO_ROUNDING, \
get_sendpayment_parser, get_max_cj_fee_values, check_regtest, \
parse_payjoin_setup, send_payjoin
parse_payjoin_setup, send_payjoin, general_custom_change_warning, \
nonwallet_custom_change_warning, sweep_custom_change_warning, \
detect_script_type, EngineError
from twisted.python.log import startLogging
from jmbase.support import get_log, jmprint, \
EXIT_FAILURE, EXIT_ARGERROR, DUST_THRESHOLD
@ -202,9 +204,46 @@ def main():
log.info("Estimated miner/tx fees for this coinjoin amount: {:.1%}"
.format(exp_tx_fees_ratio))
custom_change = None
if options.customchange != '':
addr_valid, errormsg = validate_address(options.customchange)
if not addr_valid:
parser.error(
"The custom change address provided is not valid\n{}".format(
errormsg))
sys.exit(EXIT_ARGERROR)
custom_change = options.customchange
if destaddr and custom_change == destaddr:
parser.error("The custom change address cannot be the same as the "
"destination address.")
sys.exit(EXIT_ARGERROR)
if sweeping:
parser.error(sweep_custom_change_warning)
sys.exit(EXIT_ARGERROR)
if bip78url:
parser.error("Custom change is not currently supported "
"with Payjoin. Please retry without a custom change address.")
sys.exit(EXIT_ARGERROR)
if options.makercount > 0:
if not options.answeryes and input(
general_custom_change_warning + " (y/n):")[0] != "y":
sys.exit(EXIT_ARGERROR)
change_spk = wallet_service.addr_to_script(custom_change)
engine_recognized = True
try:
change_addr_type = detect_script_type(change_spk)
except EngineError:
engine_recognized = False
if (not engine_recognized) or (
change_addr_type != wallet_service.TYPE):
if not options.answeryes and input(
nonwallet_custom_change_warning + " (y/n):")[0] != "y":
sys.exit(EXIT_ARGERROR)
if options.makercount == 0 and not bip78url:
tx = direct_send(wallet_service, amount, mixdepth, destaddr,
options.answeryes, with_final_psbt=options.with_psbt)
options.answeryes, with_final_psbt=options.with_psbt,
custom_change_addr=custom_change)
if options.with_psbt:
log.info("This PSBT is fully signed and can be sent externally for "
"broadcasting:")
@ -315,7 +354,8 @@ def main():
schedule,
order_chooser=chooseOrdersFunc,
max_cj_fee=maxcjfee,
callbacks=(filter_orders_callback, None, taker_finished))
callbacks=(filter_orders_callback, None, taker_finished),
custom_change_address=custom_change)
clientfactory = JMClientProtocolFactory(taker)
if jm_single().config.get("BLOCKCHAIN", "network") == "regtest":

Loading…
Cancel
Save