@ -19,13 +19,14 @@ import json
import random
from io import BytesIO
from pprint import pformat
from jmbase import BytesProducer , bintohex , jmprint
from jmbase import bintohex , jmprint
from . configure import get_log , jm_single
import jmbitcoin as btc
from . wallet import PSBTWalletMixin , SegwitLegacyWallet , SegwitWallet , estimate_tx_fee
from . wallet_service import WalletService
from . taker_utils import direct_send
from jmclient import RegtestBitcoinCoreInterface , select_one_utxo , process_shutdown
from jmclient import ( RegtestBitcoinCoreInterface , select_one_utxo ,
process_shutdown , BIP78ClientProtocolFactory )
"""
For some documentation see :
@ -43,36 +44,6 @@ INPUT_VSIZE_LEGACY = 148
INPUT_VSIZE_SEGWIT_LEGACY = 91
INPUT_VSIZE_SEGWIT_NATIVE = 68
# txtorcon outputs erroneous warnings about hiddenservice directory strings,
# annoyingly, so we suppress it here:
import warnings
warnings . filterwarnings ( " ignore " )
""" This whitelister allows us to accept any cert for a specific
domain , and is to be used for testing only ; the default Agent
behaviour of twisted . web . client . Agent for https URIs is
the correct one in production ( i . e . uses local trust store ) .
"""
@implementer ( IPolicyForHTTPS )
class WhitelistContextFactory ( object ) :
def __init__ ( self , good_domains = None ) :
"""
: param good_domains : List of domains . The URLs must be in bytes
"""
if not good_domains :
self . good_domains = [ ]
else :
self . good_domains = good_domains
# by default, handle requests like a browser would
self . default_policy = BrowserLikePolicyForHTTPS ( )
def creatorForNetloc ( self , hostname , port ) :
# check if the hostname is in the the whitelist,
# otherwise return the default policy
if hostname in self . good_domains :
return CertificateOptions ( verify = False )
return self . default_policy . creatorForNetloc ( hostname , port )
class JMPayjoinManager ( object ) :
""" An encapsulation of state for an
ongoing Payjoin payment . Allows reporting
@ -435,6 +406,7 @@ class JMPayjoinManager(object):
self . user_info_callback ( " Choosing one coin at random " )
try :
print ( ' attempting to select from mixdepth: ' , str ( self . mixdepth ) )
my_utxos = self . wallet_service . select_utxos (
self . mixdepth , jm_single ( ) . DUST_THRESHOLD ,
select_fn = select_one_utxo )
@ -519,76 +491,32 @@ def get_max_additional_fee_contribution(manager):
" contribution of: " + str ( max_additional_fee_contribution ) )
return max_additional_fee_contribution
def send_payjoin ( manager , accept_callback = None ,
info_callback = None , return_deferred = False ) :
""" Given a JMPayjoinManager object `manager`, initialised with the
payment request data from the server , use its wallet_service to construct
a payment transaction , with coins sourced from mixdepth ` mixdepth ` ,
then wait for the server response , parse the PSBT , perform checks and complete sign .
The info and accept callbacks are to ask the user to confirm the creation of
the original payment transaction ( None defaults to terminal / CLI processing ) ,
and are as defined in ` taker_utils . direct_send ` .
Returns :
( True , None ) in case of payment setup successful ( response will be delivered
asynchronously ) - the ` manager ` object can be inspected for more detail .
( False , errormsg ) in case of failure .
def make_payment_psbt ( manager , accept_callback = None , info_callback = None ) :
""" Creates a valid payment transaction and PSBT for it,
and adds it to the JMPayjoinManager instance passed as argument .
Wallet should already be synced before calling here .
Returns True , None if successful or False , errormsg if not .
"""
# wallet should already be synced before calling here;
# we can create a standard payment, but have it returned as a PSBT.
assert isinstance ( manager , JMPayjoinManager )
assert manager . wallet_service . synced
payment_psbt = direct_send ( manager . wallet_service , manager . amount , manager . mixdepth ,
str ( manager . destination ) , accept_callback = accept_callback ,
info_callback = info_callback ,
with_final_psbt = True , optin_rbf = True )
payment_psbt = direct_send ( manager . wallet_service , manager . amount ,
manager . mixdepth , str ( manager . destination ) ,
accept_callback = accept_callback ,
info_callback = info_callback ,
with_final_psbt = True , optin_rbf = True )
if not payment_psbt :
return ( False , " could not create non-payjoin payment " )
# TLS whitelist is for regtest testing, it is treated as hostnames for
# which tls certificate verification is ignored.
tls_whitelist = None
if isinstance ( jm_single ( ) . bc_interface , RegtestBitcoinCoreInterface ) :
tls_whitelist = [ b " 127.0.0.1 " ]
manager . set_payment_tx_and_psbt ( payment_psbt )
# add delayed call to broadcast this after 1 minute
manager . timeout_fallback_dc = reactor . callLater ( 60 ,
fallback_nonpayjoin_broadcast ,
b " timeout " , manager )
# Now we send the request to the server, with the encoded
# payment PSBT
# First we create a twisted web Agent object:
# TODO genericize/move out/use library function:
def is_hs_uri ( s ) :
x = urlparse . urlparse ( s )
if x . hostname . endswith ( " .onion " ) :
return ( x . scheme , x . hostname , x . port )
return False
tor_url_data = is_hs_uri ( manager . server )
if tor_url_data :
# note the return value is currently unused here
socks5_host = jm_single ( ) . config . get ( " PAYJOIN " , " onion_socks5_host " )
socks5_port = int ( jm_single ( ) . config . get ( " PAYJOIN " , " onion_socks5_port " ) )
# note: SSL not supported at the moment:
torEndpoint = TCP4ClientEndpoint ( reactor , socks5_host , socks5_port )
agent = tor_agent ( reactor , torEndpoint )
else :
if not tls_whitelist :
agent = Agent ( reactor )
else :
agent = Agent ( reactor ,
contextFactory = WhitelistContextFactory ( tls_whitelist ) )
body = BytesProducer ( payment_psbt . to_base64 ( ) . encode ( " utf-8 " ) )
return ( True , None )
#Set the query parameters for the request:
def make_payjoin_request_params ( manager ) :
""" Returns the query parameters for the request
to the payjoin receiver , based on the configuration
of the given JMPayjoinManager instance .
"""
# construct the URI from the given parameters
pj_version = jm_single ( ) . config . getint ( " PAYJOIN " ,
@ -614,27 +542,39 @@ def send_payjoin(manager, accept_callback=None,
min_fee_rate = float ( jm_single ( ) . config . get ( " PAYJOIN " , " min_fee_rate " ) )
params [ " minfeerate " ] = min_fee_rate
destination_url = manager . server . encode ( " utf-8 " )
url_parts = list ( urlparse . urlparse ( destination_url ) )
url_parts [ 4 ] = urlencode ( params ) . encode ( " utf-8 " )
destination_url = urlparse . urlunparse ( url_parts )
# TODO what to use as user agent?
d = agent . request ( b " POST " , destination_url ,
Headers ( { " User-Agent " : [ " Twisted Web Client Example " ] ,
" Content-Type " : [ " text/plain " ] } ) ,
bodyProducer = body )
d . addCallback ( receive_payjoin_proposal_from_server , manager )
# note that the errback (here "noResponse") is *not* triggered
# by a server rejection (which is accompanied by a non-200
# status code returned), but by failure to communicate.
def noResponse ( failure ) :
failure . trap ( ResponseFailed , ConnectionRefusedError ,
HostUnreachableError , ConnectionLost )
log . error ( failure . value )
fallback_nonpayjoin_broadcast ( b " connection failed " , manager )
d . addErrback ( noResponse )
if return_deferred :
return d
return params
def send_payjoin ( manager , accept_callback = None ,
info_callback = None , return_deferred = False ) :
""" Given a JMPayjoinManager object `manager`, initialised with the
payment request data from the server , use its wallet_service to construct
a payment transaction , with coins sourced from mixdepth ` manager . mixdepth ` ,
then wait for the server response , parse the PSBT , perform checks and complete sign .
The info and accept callbacks are to ask the user to confirm the creation of
the original payment transaction ( None defaults to terminal / CLI processing ) ,
and are as defined in ` taker_utils . direct_send ` .
Returns :
( True , None ) in case of payment setup successful ( response will be delivered
asynchronously ) - the ` manager ` object can be inspected for more detail .
( False , errormsg ) in case of failure .
"""
success , errmsg = make_payment_psbt ( manager , accept_callback , info_callback )
if not success :
return ( False , errmsg )
# add delayed call to broadcast this after 1 minute
manager . timeout_fallback_dc = reactor . callLater ( 60 ,
fallback_nonpayjoin_broadcast ,
b " timeout " , manager )
params = make_payjoin_request_params ( manager )
factory = BIP78ClientProtocolFactory ( manager , params ,
process_payjoin_proposal_from_server , process_error_from_server )
# TODO add SSL option as for other protocol instances:
reactor . connectTCP ( jm_single ( ) . config . get ( " DAEMON " , " daemon_host " ) ,
jm_single ( ) . config . getint ( " DAEMON " , " daemon_port " ) - 2000 ,
factory )
return ( True , None )
def fallback_nonpayjoin_broadcast ( err , manager ) :
@ -667,19 +607,14 @@ def fallback_nonpayjoin_broadcast(err, manager):
manager . timeout_fallback_dc . cancel ( )
quit ( )
def receive_payjoin_proposal_from_server ( respons e, manager ) :
def process_error_from_server ( errormsg , errorcod e, manager ) :
assert isinstance ( manager , JMPayjoinManager )
# no attempt at chunking or handling incrementally is needed
# here. The body should be a byte string containing the
# new PSBT, or a jsonified error page.
d = readBody ( response )
# if the response code is not 200 OK, we must assume payjoin
# attempt has failed, and revert to standard payment.
if int ( response . code ) != 200 :
log . warn ( " Receiver returned error code: " + str ( response . code ) )
d . addCallback ( fallback_nonpayjoin_broadcast , manager )
return
d . addCallback ( process_payjoin_proposal_from_server , manager )
# payjoin attempt has failed, we revert to standard payment.
assert int ( errorcode ) != 200
log . warn ( " Receiver returned error code: {} , message: {} " . format (
errorcode , errormsg ) )
fallback_nonpayjoin_broadcast ( errormsg . encode ( " utf-8 " ) , manager )
return
def process_payjoin_proposal_from_server ( response_body , manager ) :
assert isinstance ( manager , JMPayjoinManager )