@ -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 . check Address( self . addressInput . text ( ) ) )
lambda : self . parseURIAndValidate Address( 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 ) :