@ -1,5 +1,6 @@
#! /usr/bin/env python
from jmbitcoin import *
import datetime
import os
import time
@ -7,6 +8,7 @@ import abc
import json
import atexit
from io import BytesIO
from jmclient . wallet_utils import wallet_showseed , wallet_showutxos
from twisted . python . log import startLogging
from twisted . internet import endpoints , reactor , ssl , task
from twisted . web . server import Site
@ -16,17 +18,21 @@ from klein import Klein
from optparse import OptionParser
from jmbase import get_log
from jmbitcoin import human_readable_transaction
from jmclient import Maker , jm_single , load_program_config , \
from jmclient import Taker , Maker , jm_single , load_program_config , \
JMClientProtocolFactory , start_reactor , calc_cj_fee , \
WalletService , add_base_options , get_wallet_path , direct_send , \
open_test_wallet_maybe , wallet_display , SegwitLegacyWallet , \
open_test_wallet_maybe , wallet , wallet _display, SegwitLegacyWallet , \
SegwitWallet , get_daemon_serving_params , YieldGeneratorService , \
SNICKERReceiverService , SNICKERReceiver , create_wallet , \
StorageError , StoragePasswordError
from jmbase . support import EXIT_ARGERROR , EXIT_FAILURE
StorageError , StoragePasswordError , get_max_cj_fee_values
from jmbase . support import get_log , set_logging_level , jmprint , EXIT_ARGERROR , EXIT_FAILURE , DUST_THRESHOLD
import glob
import jwt
jlog = get_log ( )
# for debugging; twisted.web.server.Request objects do not easily serialize:
def print_req ( request ) :
print ( request )
@ -91,6 +97,7 @@ class JMWalletDaemon(Service):
""" Port is the port to serve this daemon
( using HTTP / TLS ) .
"""
print ( " in init " )
# cookie tracks single user's state.
self . cookie = None
self . port = port
@ -167,9 +174,24 @@ class JMWalletDaemon(Service):
request . setResponseCode ( 401 )
return " Service cannot be stopped as it is not running. "
# def check_cookie(self, request):
# request_cookie = request.getHeader(b"JMCookie")
# if self.cookie != request_cookie:
# jlog.warn("Invalid cookie: " + str(
# request_cookie) + ", request rejected.")
# raise NotAuthorized()
def check_cookie ( self , request ) :
request_cookie = request . getHeader ( b " JMCookie " )
if self . cookie != request_cookie :
print ( " header details: " )
#part after bearer is what we need
auth_header = ( ( request . getHeader ( ' Authorization ' ) ) )
request_cookie = None
if auth_header is not None :
request_cookie = auth_header [ 7 : ]
print ( " request cookie is " , request_cookie )
print ( " actual cookie is " , self . cookie )
if request_cookie == None or self . cookie != request_cookie :
jlog . warn ( " Invalid cookie: " + str (
request_cookie ) + " , request rejected. " )
raise NotAuthorized ( )
@ -185,6 +207,16 @@ class JMWalletDaemon(Service):
walletinfo = wallet_display ( self . wallet_service , False , jsonified = True )
return response ( request , walletname = walletname , walletinfo = walletinfo )
#Heartbeat route
@app . route ( ' /session ' , methods = [ ' GET ' ] )
def sessionExists ( self , request ) :
#if no wallet loaded then clear frontend session info
#when no wallet status is false
session = not self . cookie == None
return response ( request , session = session )
# handling CORS preflight for any route:
@app . route ( ' / ' , branch = True , methods = [ ' OPTIONS ' ] )
def preflight ( self , request ) :
@ -224,17 +256,26 @@ class JMWalletDaemon(Service):
""" Use the contents of the POST body to do a direct send from
the active wallet at the chosen mixdepth .
"""
self . check_cookie ( request )
assert isinstance ( request . content , BytesIO )
payment_info_json = self . get_POST_body ( request , [ " mixdepth " , " amount_sats " ,
" destination " ] )
if not payment_info_json :
raise InvalidRequestFormat ( )
if not self . wallet_service :
raise NoWalletFound ( )
tx = direct_send ( self . wallet_service , payment_info_json [ " amount_sats " ] ,
payment_info_json [ " mixdepth " ] ,
optin_rbf = payment_info_json [ " optin_rbf " ] ,
return_transaction = True )
tx = direct_send ( self . wallet_service , int ( payment_info_json [ " amount_sats " ] ) ,
int ( payment_info_json [ " mixdepth " ] ) ,
destination = payment_info_json [ " destination " ] ,
return_transaction = True , answeryes = True )
# tx = direct_send(self.wallet_service, payment_info_json["amount_sats"],
# payment_info_json["mixdepth"],
# optin_rbf=payment_info_json["optin_rbf"],
# return_transaction=True)
return response ( request , walletname = walletname ,
txinfo = human_readable_transaction ( tx ) )
@ -242,6 +283,7 @@ class JMWalletDaemon(Service):
def start_maker ( self , request , walletname ) :
""" Use the configuration in the POST body to start the yield generator:
"""
self . check_cookie ( request )
assert isinstance ( request . content , BytesIO )
config_json = self . get_POST_body ( request , [ " txfee " , " cjfee_a " , " cjfee_r " ,
" ordertype " , " minsize " ] )
@ -255,10 +297,21 @@ class JMWalletDaemon(Service):
if daemon_serving_port == - 1 or daemon_serving_host == " " :
raise BackendNotReady ( )
for key , val in config_json . items ( ) :
if ( key == ' cjfee_r ' or key == ' ordertype ' ) :
pass
else :
config_json [ key ] = int ( config_json [ key ] )
# self.txfee_factor, self.cjfee_factor, self.size_factor
config_json [ ' txfee_factor ' ] = None
config_json [ " cjfee_factor " ] = None
config_json [ " size_factor " ] = None
self . services [ " maker " ] = YieldGeneratorService ( self . wallet_service ,
daemon_serving_host , daemon_serving_port ,
[ config_json [ x ] for x in [ " txfee " , " cjfee_a " ,
" cjfee_r " , " ordertype " , " minsize " ] ] )
" cjfee_r " , " ordertype " , " minsize " , " txfee_factor " , " cjfee_factor " , " size_factor " ] ] )
self . services [ " maker " ] . startService ( )
return response ( request , walletname = walletname )
@ -281,6 +334,7 @@ class JMWalletDaemon(Service):
raise NoWalletFound ( )
else :
self . wallet_service . stopService ( )
self . cookie = None
self . wallet_service = None
# success status implicit:
return response ( request , walletname = walletname )
@ -313,6 +367,8 @@ class JMWalletDaemon(Service):
request_data = self . get_POST_body ( request ,
[ " walletname " , " password " , " wallettype " ] )
if not request_data or request_data [ " wallettype " ] not in [
" sw " , " sw-legacy " ] :
raise InvalidRequestFormat ( )
@ -324,17 +380,25 @@ class JMWalletDaemon(Service):
# data to construct the wallet path:
wallet_root_path = os . path . join ( jm_single ( ) . datadir , " wallets " )
wallet_name = os . path . join ( wallet_root_path , request_data [ " walletname " ] )
try :
wallet = create_wallet ( wallet_name , request_data [ " password " ] ,
wallet = create_wallet ( wallet_name , request_data [ " password " ] . encode ( " ascii " ) ,
4 , wallet_cls = wallet_cls )
print ( " seedphrase is " )
seedphrase_help_string = wallet_showseed ( wallet )
except StorageError as e :
raise NotAuthorized ( repr ( e ) )
# finally, after the wallet is successfully created, we should
# start the wallet service:
return self . initialize_wallet_service ( request , wallet )
def initialize_wallet_service ( self , request , wallet ) :
#return response(request,message="Wallet Created Succesfully,unlock it for further use")
return self . initialize_wallet_service ( request , wallet , seedphrase = seedphrase_help_string )
def initialize_wallet_service ( self , request , wallet , * * kwargs ) :
""" Called only when the wallet has loaded correctly, so
authorization is passed , so set cookie for this wallet
( currently THE wallet , daemon does not yet support multiple ) .
@ -342,7 +406,18 @@ class JMWalletDaemon(Service):
no expiry currently implemented ) , or until the user switches
to a new wallet .
"""
self . cookie = request . getHeader ( b " JMCookie " )
encoded_token = jwt . encode ( { " wallet " : " name_of_wallet " , " exp " : datetime . datetime . utcnow ( ) + datetime . timedelta ( minutes = 30 ) } , " secret " )
encoded_token = encoded_token . strip ( )
print ( encoded_token )
# decoded_token = jwt.decode(encoded_token,"secret",algorithms=["HS256"])
# print(decoded_token)
# request.addCookie(b'session_token', encoded_token)
# self.cookie = encoded_token
self . cookie = encoded_token
#self.cookie = request.getHeader(b"JMCookie")
if self . cookie is None :
raise NotAuthorized ( " No cookie " )
@ -355,13 +430,21 @@ class JMWalletDaemon(Service):
self . wallet_service . startService ( )
# now that the WalletService instance is active and ready to
# respond to requests, we return the status to the client:
return response ( request ,
#def response(request, succeed=True, status=200, **kwargs):
if ( ' seedphrase ' in kwargs ) :
return response ( request ,
walletname = self . wallet_service . get_wallet_name ( ) ,
already_loaded = False )
already_loaded = False , token = encoded_token , seedphrase = kwargs . get ( ' seedphrase ' ) )
else :
return response ( request ,
walletname = self . wallet_service . get_wallet_name ( ) ,
already_loaded = False , token = encoded_token )
@app . route ( ' /wallet/<string:walletname>/unlock ' , methods = [ ' POST ' ] )
def unlockwallet ( self , request , walletname ) :
print_req ( request )
#print(get_current_chain_params())
assert isinstance ( request . content , BytesIO )
auth_json = self . get_POST_body ( request , [ " password " ] )
if not auth_json :
@ -386,6 +469,144 @@ class JMWalletDaemon(Service):
walletname = self . wallet_service . get_wallet_name ( ) ,
already_loaded = True )
#This route should return list of current wallets created.
@app . route ( ' /wallet/all ' , methods = [ ' GET ' ] )
def listwallets ( self , request ) :
#this is according to the assumption that wallets are there in /.joinmarket by default, also currently path for linux system only.
#first user taken for path
user_path = glob . glob ( ' /home/*/ ' ) [ 0 ]
wallet_dir = f " { user_path } .joinmarket/wallets/*.jmdat "
wallets = ( glob . glob ( wallet_dir ) )
offset = len ( user_path ) + len ( ' .joinmarket/wallets/ ' )
#to get only names
short_wallets = [ wallet [ offset : ] for wallet in wallets ]
return response ( request , wallets = short_wallets )
#route to get external address for deposit
@app . route ( ' /address/new/<string:mixdepth> ' , methods = [ ' GET ' ] )
def getaddress ( self , request , mixdepth ) :
self . check_cookie ( request )
if not self . wallet_service :
raise NoWalletFound ( )
mixdepth = int ( mixdepth )
address = self . wallet_service . get_external_addr ( mixdepth )
return response ( request , address = address )
#route to list utxos
@app . route ( ' /wallet/utxos ' , methods = [ ' GET ' ] )
def listUtxos ( self , request ) :
self . check_cookie ( request )
if not self . wallet_service :
raise NoWalletFound ( )
utxos = wallet_showutxos ( self . wallet_service , False )
return response ( request , transactions = utxos )
#return True for now
def filter_orders_callback ( self , orderfees , cjamount ) :
return True
#route to start a coinjoin transaction
@app . route ( ' /wallet/taker/coinjoin ' , methods = [ ' POST ' ] )
def doCoinjoin ( self , request ) :
self . check_cookie ( request )
if not self . wallet_service :
raise NoWalletFound ( )
request_data = self . get_POST_body ( request , [ " mixdepth " , " amount " , " counterparties " , " destination " ] )
#refer sample schedule testnet
waittime = 0
rounding = 16
completion_flag = 0
#list of list
schedule = [ [ int ( request_data [ " mixdepth " ] ) , int ( request_data [ " amount " ] ) , int ( request_data [ " counterparties " ] ) , request_data [ " destination " ] , waittime , rounding , completion_flag ] ]
print ( schedule )
#instantiate a taker
#keeping order_chooser as default for now
#max_cj_feee is to be set based on config values (jmsingle.config.get policy var->max cj fee abs in configure.py)
max_cj_fee = ( 1 , float ( ' inf ' ) )
print ( " max cj fee is, " , max_cj_fee )
self . taker = Taker ( self . wallet_service , schedule , max_cj_fee = max_cj_fee , callbacks = ( self . filter_orders_callback , None , self . taker_finished ) )
clientfactory = JMClientProtocolFactory ( self . taker )
nodaemon = jm_single ( ) . config . getint ( " DAEMON " , " no_daemon " )
daemon = True if nodaemon == 1 else False
dhost = jm_single ( ) . config . get ( " DAEMON " , " daemon_host " )
dport = jm_single ( ) . config . getint ( " DAEMON " , " daemon_port " )
if jm_single ( ) . config . get ( " BLOCKCHAIN " , " network " ) == " regtest " :
startLogging ( sys . stdout )
start_reactor ( dhost , dport , clientfactory , daemon = daemon , rs = False )
def taker_finished ( self , res , fromtx = False , waittime = 0.0 , txdetails = None ) :
if fromtx == " unconfirmed " :
#If final entry, stop *here*, don't wait for confirmation
return
if fromtx :
if res :
txd , txid = txdetails
reactor . callLater ( waittime * 60 ,
clientfactory . getClient ( ) . clientStart )
else :
#a transaction failed; we'll try to repeat without the
#troublemakers.
#If this error condition is reached from Phase 1 processing,
#and there are less than minimum_makers honest responses, we
#just give up (note that in tumbler we tweak and retry, but
#for sendpayment the user is "online" and so can manually
#try again).
#However if the error is in Phase 2 and we have minimum_makers
#or more responses, we do try to restart with the honest set, here.
if self . taker . latest_tx is None :
#can only happen with < minimum_makers; see above.
jlog . info ( " A transaction failed but there are insufficient "
" honest respondants to continue; giving up. " )
reactor . stop ( )
return
#This is Phase 2; do we have enough to try again?
self . taker . add_honest_makers ( list ( set (
self . taker . maker_utxo_data . keys ( ) ) . symmetric_difference (
set ( self . taker . nonrespondants ) ) ) )
if len ( self . taker . honest_makers ) < jm_single ( ) . config . getint (
" POLICY " , " minimum_makers " ) :
jlog . info ( " Too few makers responded honestly; "
" giving up this attempt. " )
reactor . stop ( )
return
jmprint ( " We failed to complete the transaction. The following "
" makers responded honestly: " + str ( self . taker . honest_makers ) + \
" , so we will retry with them. " , " warning " )
#Now we have to set the specific group we want to use, and hopefully
#they will respond again as they showed honesty last time.
#we must reset the number of counterparties, as well as fix who they
#are; this is because the number is used to e.g. calculate fees.
#cleanest way is to reset the number in the schedule before restart.
self . taker . schedule [ self . taker . schedule_index ] [ 2 ] = len ( self . taker . honest_makers )
jlog . info ( " Retrying with: " + str ( self . taker . schedule [
self . taker . schedule_index ] [ 2 ] ) + " counterparties. " )
#rewind to try again (index is incremented in Taker.initialize())
self . taker . schedule_index - = 1
self . taker . set_honest_only ( True )
reactor . callLater ( 5.0 , clientfactory . getClient ( ) . clientStart )
else :
if not res :
jlog . info ( " Did not complete successfully, shutting down " )
#Should usually be unreachable, unless conf received out of order;
#because we should stop on 'unconfirmed' for last (see above)
else :
jlog . info ( " All transactions completed correctly " )
reactor . stop ( )
def jmwalletd_main ( ) :
import sys
parser = OptionParser ( usage = ' usage: % prog [options] [wallet file] ' )
@ -416,5 +637,7 @@ def jmwalletd_main():
jm_single ( ) . config . getint ( " DAEMON " , " daemon_port " ) ,
None , daemon = daemon )
if __name__ == " __main__ " :
jmwalletd_main ( )