|
|
|
@ -10,6 +10,7 @@ from twisted.application.service import Service |
|
|
|
from autobahn.twisted.websocket import listenWS |
|
|
|
from autobahn.twisted.websocket import listenWS |
|
|
|
from klein import Klein |
|
|
|
from klein import Klein |
|
|
|
import jwt |
|
|
|
import jwt |
|
|
|
|
|
|
|
import pprint |
|
|
|
|
|
|
|
|
|
|
|
from jmbitcoin import human_readable_transaction |
|
|
|
from jmbitcoin import human_readable_transaction |
|
|
|
from jmclient import Taker, jm_single, \ |
|
|
|
from jmclient import Taker, jm_single, \ |
|
|
|
@ -20,7 +21,11 @@ from jmclient import Taker, jm_single, \ |
|
|
|
create_wallet, get_max_cj_fee_values, \ |
|
|
|
create_wallet, get_max_cj_fee_values, \ |
|
|
|
StorageError, StoragePasswordError, JmwalletdWebSocketServerFactory, \ |
|
|
|
StorageError, StoragePasswordError, JmwalletdWebSocketServerFactory, \ |
|
|
|
JmwalletdWebSocketServerProtocol, RetryableStorageError, \ |
|
|
|
JmwalletdWebSocketServerProtocol, RetryableStorageError, \ |
|
|
|
SegwitWalletFidelityBonds, wallet_gettimelockaddress, NotEnoughFundsException |
|
|
|
SegwitWalletFidelityBonds, wallet_gettimelockaddress, \ |
|
|
|
|
|
|
|
NotEnoughFundsException, get_tumble_log, get_tumble_schedule, \ |
|
|
|
|
|
|
|
get_schedule, get_tumbler_parser, schedule_to_text, \ |
|
|
|
|
|
|
|
tumbler_filter_orders_callback, tumbler_taker_finished_update, \ |
|
|
|
|
|
|
|
validate_address |
|
|
|
from jmbase.support import get_log, utxostr_to_utxo |
|
|
|
from jmbase.support import get_log, utxostr_to_utxo |
|
|
|
|
|
|
|
|
|
|
|
jlog = get_log() |
|
|
|
jlog = get_log() |
|
|
|
@ -158,6 +163,10 @@ class JMWalletDaemon(Service): |
|
|
|
# keep track of client side connections so they |
|
|
|
# keep track of client side connections so they |
|
|
|
# can be shut down cleanly: |
|
|
|
# can be shut down cleanly: |
|
|
|
self.coinjoin_connection = None |
|
|
|
self.coinjoin_connection = None |
|
|
|
|
|
|
|
# Options for generating a tumble schedule / running the tumbler. |
|
|
|
|
|
|
|
# Doubles as flag for indicating whether we're currently running |
|
|
|
|
|
|
|
# a tumble schedule. |
|
|
|
|
|
|
|
self.tumbler_options = None |
|
|
|
|
|
|
|
|
|
|
|
def get_client_factory(self): |
|
|
|
def get_client_factory(self): |
|
|
|
return JMClientProtocolFactory(self.taker) |
|
|
|
return JMClientProtocolFactory(self.taker) |
|
|
|
@ -424,14 +433,26 @@ class JMWalletDaemon(Service): |
|
|
|
token=self.cookie) |
|
|
|
token=self.cookie) |
|
|
|
|
|
|
|
|
|
|
|
def taker_finished(self, res, fromtx=False, waittime=0.0, txdetails=None): |
|
|
|
def taker_finished(self, res, fromtx=False, waittime=0.0, txdetails=None): |
|
|
|
# This is a slimmed down version compared with what is seen in |
|
|
|
if not self.tumbler_options: |
|
|
|
# the CLI code, since that code encompasses schedules with multiple |
|
|
|
# We were doing a single coinjoin -- stop taker. |
|
|
|
# entries; for now, the RPC only supports single joins. |
|
|
|
self.stop_taker(res) |
|
|
|
# TODO this may be updated. |
|
|
|
else: |
|
|
|
# It is also different in that the event loop must not shut down |
|
|
|
# We're running the tumbler. |
|
|
|
# when processing finishes. |
|
|
|
logsdir = os.path.join(os.path.dirname(jm_single().config_location), "logs") |
|
|
|
|
|
|
|
tumble_log = get_tumble_log(logsdir) |
|
|
|
# reset our state on completion, we are no longer coinjoining: |
|
|
|
sfile = os.path.join(logsdir, self.tumbler_options['schedulefile']) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tumbler_taker_finished_update(self.taker, sfile, tumble_log, self.tumbler_options, res, fromtx, waittime, txdetails) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not fromtx: |
|
|
|
|
|
|
|
# The tumbling schedule's final transaction is done. |
|
|
|
|
|
|
|
self.stop_taker(res) |
|
|
|
|
|
|
|
self.tumbler_options = None |
|
|
|
|
|
|
|
elif fromtx != "unconfirmed": |
|
|
|
|
|
|
|
# A non-final transaction in the tumbling schedule is done -- continue schedule after wait time. |
|
|
|
|
|
|
|
reactor.callLater(waittime*60, self.clientfactory.getClient().clientStart) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def stop_taker(self, res): |
|
|
|
self.taker = None |
|
|
|
self.taker = None |
|
|
|
|
|
|
|
|
|
|
|
if not res: |
|
|
|
if not res: |
|
|
|
@ -464,6 +485,9 @@ class JMWalletDaemon(Service): |
|
|
|
""" |
|
|
|
""" |
|
|
|
return True |
|
|
|
return True |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def filter_orders_callback_tumbler(self, orders_fees, cjamount): |
|
|
|
|
|
|
|
return tumbler_filter_orders_callback(orders_fees, cjamount, self.taker) |
|
|
|
|
|
|
|
|
|
|
|
def check_daemon_ready(self): |
|
|
|
def check_daemon_ready(self): |
|
|
|
# daemon must be up before coinjoins start. |
|
|
|
# daemon must be up before coinjoins start. |
|
|
|
daemon_serving_host, daemon_serving_port = get_daemon_serving_params() |
|
|
|
daemon_serving_host, daemon_serving_port = get_daemon_serving_params() |
|
|
|
@ -1023,3 +1047,111 @@ class JMWalletDaemon(Service): |
|
|
|
raise InvalidRequestFormat() |
|
|
|
raise InvalidRequestFormat() |
|
|
|
seedphrase, _ = self.services["wallet"].get_mnemonic_words() |
|
|
|
seedphrase, _ = self.services["wallet"].get_mnemonic_words() |
|
|
|
return make_jmwalletd_response(request, seedphrase=seedphrase) |
|
|
|
return make_jmwalletd_response(request, seedphrase=seedphrase) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/wallet/<string:walletname>/taker/schedule', methods=['POST']) |
|
|
|
|
|
|
|
def start_tumbler(self, request, walletname): |
|
|
|
|
|
|
|
self.check_cookie(request) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not self.services["wallet"]: |
|
|
|
|
|
|
|
raise NoWalletFound() |
|
|
|
|
|
|
|
if not self.wallet_name == walletname: |
|
|
|
|
|
|
|
raise InvalidRequestFormat() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# -- Options parsing ----------------------------------------------- |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(options, args) = get_tumbler_parser().parse_args([]) |
|
|
|
|
|
|
|
self.tumbler_options = vars(options) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# At the moment only the destination addresses can be set. |
|
|
|
|
|
|
|
# For now, all other options are hardcoded to the defaults of |
|
|
|
|
|
|
|
# the tumbler.py script. |
|
|
|
|
|
|
|
request_data = self.get_POST_body(request, ["destination_addresses"]) |
|
|
|
|
|
|
|
if not request_data: |
|
|
|
|
|
|
|
raise InvalidRequestFormat() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
destaddrs = request_data["destination_addresses"] |
|
|
|
|
|
|
|
for daddr in destaddrs: |
|
|
|
|
|
|
|
success, _ = validate_address(daddr) |
|
|
|
|
|
|
|
if not success: |
|
|
|
|
|
|
|
raise InvalidRequestFormat() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Setting max_cj_fee based on global config. |
|
|
|
|
|
|
|
# We won't respect it being set via tumbler_options for now. |
|
|
|
|
|
|
|
def dummy_user_callback(rel, abs): |
|
|
|
|
|
|
|
raise ConfigNotPresent() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
max_cj_fee = get_max_cj_fee_values(jm_single().config, |
|
|
|
|
|
|
|
None, |
|
|
|
|
|
|
|
user_callback=dummy_user_callback) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
jm_single().mincjamount = self.tumbler_options['mincjamount'] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# -- Schedule generation ------------------------------------------- |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Always generates a new schedule. No restart support for now. |
|
|
|
|
|
|
|
schedule = get_tumble_schedule(self.tumbler_options, |
|
|
|
|
|
|
|
destaddrs, |
|
|
|
|
|
|
|
self.services["wallet"].get_balance_by_mixdepth()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logsdir = os.path.join(os.path.dirname(jm_single().config_location), |
|
|
|
|
|
|
|
"logs") |
|
|
|
|
|
|
|
sfile = os.path.join(logsdir, self.tumbler_options['schedulefile']) |
|
|
|
|
|
|
|
with open(sfile, "wb") as f: |
|
|
|
|
|
|
|
f.write(schedule_to_text(schedule)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tumble_log = get_tumble_log(logsdir) |
|
|
|
|
|
|
|
tumble_log.info("TUMBLE STARTING") |
|
|
|
|
|
|
|
tumble_log.info("With this schedule: ") |
|
|
|
|
|
|
|
tumble_log.info(pprint.pformat(schedule)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# -- Running the Taker --------------------------------------------- |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# For simplicity, we're not doing any fee estimation for now. |
|
|
|
|
|
|
|
# We might want to add fee estimation (see scripts/tumbler.py) to |
|
|
|
|
|
|
|
# prevent users from overspending on fees when tumbling with small |
|
|
|
|
|
|
|
# amounts. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not self.activate_coinjoin_state(CJ_TAKER_RUNNING): |
|
|
|
|
|
|
|
raise ServiceAlreadyStarted() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.taker = Taker(self.services["wallet"], |
|
|
|
|
|
|
|
schedule, |
|
|
|
|
|
|
|
max_cj_fee=max_cj_fee, |
|
|
|
|
|
|
|
order_chooser=self.tumbler_options['order_choose_fn'], |
|
|
|
|
|
|
|
callbacks=(self.filter_orders_callback_tumbler, None, |
|
|
|
|
|
|
|
self.taker_finished), |
|
|
|
|
|
|
|
tdestaddrs=destaddrs) |
|
|
|
|
|
|
|
self.clientfactory = self.get_client_factory() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.taker.testflag = True |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dhost, dport = self.check_daemon_ready() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_, self.coinjoin_connection = start_reactor(dhost, |
|
|
|
|
|
|
|
dport, |
|
|
|
|
|
|
|
self.clientfactory, |
|
|
|
|
|
|
|
rs=False) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return make_jmwalletd_response(request, status=202, schedule=schedule) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/wallet/<string:walletname>/taker/schedule', methods=['GET']) |
|
|
|
|
|
|
|
def get_tumbler_schedule(self, request, walletname): |
|
|
|
|
|
|
|
self.check_cookie(request) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not self.services["wallet"]: |
|
|
|
|
|
|
|
raise NoWalletFound() |
|
|
|
|
|
|
|
if not self.wallet_name == walletname: |
|
|
|
|
|
|
|
raise InvalidRequestFormat() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not self.tumbler_options: |
|
|
|
|
|
|
|
return make_jmwalletd_response(request, status=404) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logsdir = os.path.join(os.path.dirname(jm_single().config_location), "logs") |
|
|
|
|
|
|
|
sfile = os.path.join(logsdir, self.tumbler_options['schedulefile']) |
|
|
|
|
|
|
|
res, schedule = get_schedule(sfile) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not res: |
|
|
|
|
|
|
|
return make_jmwalletd_response(request, status=500) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return make_jmwalletd_response(request, schedule=schedule) |
|
|
|
|