def load_program_config(config_path=None, bs=None):
    global_singleton.config.readfp(io.BytesIO(defaultconfig))
    if not config_path:
        config_path = os.getcwd()
    global_singleton.config_location = os.path.join(
        config_path, global_singleton.config_location)
    loadedFiles = global_singleton.config.read(
        [global_singleton.config_location])
    #Hack required for electrum; must be able to enforce a different
    #blockchain interface even in default/new load.
    if bs:
        global_singleton.config.set("BLOCKCHAIN", "blockchain_source", bs)
    # Create default config file if not found
    if len(loadedFiles) != 1:
        with open(global_singleton.config_location, "w") as configfile:
            configfile.write(defaultconfig)

    # check for sections
    #These are left as sanity checks but currently impossible
    #since any edits are overlays to the default, these sections/options will
    #always exist.
    for s in required_options:  #pragma: no cover
        if s not in global_singleton.config.sections():
            raise Exception(
                "Config file does not contain the required section: " + s)
    # then check for specific options
    for k, v in required_options.iteritems():  #pragma: no cover
        for o in v:
            if o not in global_singleton.config.options(k):
                raise Exception(
                    "Config file does not contain the required option: " + o)

    loglevel = global_singleton.config.get("LOGGING", "console_log_level")
    try:
        set_logging_level(loglevel)
    except:
        print(
            "Failed to set logging level, must be DEBUG, INFO, WARNING, ERROR")
    try:
        global_singleton.maker_timeout_sec = global_singleton.config.getint(
            'TIMEOUT', 'maker_timeout_sec')
    except NoOptionError:  #pragma: no cover
        log.debug('TIMEOUT/maker_timeout_sec not found in .cfg file, '
                  'using default value')

    # configure the interface to the blockchain on startup
    global_singleton.bc_interface = get_blockchain_interface_instance(
        global_singleton.config)

    #set the location of the commitments file
    try:
        global_singleton.commit_file_location = global_singleton.config.get(
            "POLICY", "commit_file_location")
    except NoOptionError:  #pragma: no cover
        log.debug("No commitment file location in config, using default "
                  "location cmtdata/commitments.json")
    set_commitment_file(
        os.path.join(config_path, global_singleton.commit_file_location))
Exemple #2
0
def load_program_config(config_path=None, bs=None):
    global_singleton.config.readfp(io.StringIO(defaultconfig))
    remove_unwanted_default_settings(global_singleton.config)
    if not config_path:
        config_path = os.getcwd()
    global_singleton.config_location = os.path.join(
        config_path, global_singleton.config_location)
    loadedFiles = global_singleton.config.read(
        [global_singleton.config_location])
    #Hack required for electrum; must be able to enforce a different
    #blockchain interface even in default/new load.
    if bs:
        global_singleton.config.set("BLOCKCHAIN", "blockchain_source", bs)
    # Create default config file if not found
    if len(loadedFiles) != 1:
        with open(global_singleton.config_location, "w") as configfile:
            configfile.write(defaultconfig)
        jmprint(
            "Created a new `joinmarket.cfg`. Please review and adopt the "
            "settings and restart joinmarket.", "info")
        exit(1)

    #These are left as sanity checks but currently impossible
    #since any edits are overlays to the default, these sections/options will
    #always exist.
    # FIXME: This check is a best-effort attempt. Certain incorrect section
    # names can pass and so can non-first invalid sections.
    for s in required_options:  #pragma: no cover
        # check for sections
        avail = None
        if not global_singleton.config.has_section(s):
            for avail in global_singleton.config.sections():
                if avail.startswith(s):
                    break
            else:
                raise Exception(
                    "Config file does not contain the required section: " + s)
        # then check for specific options
        k = avail or s
        for o in required_options[s]:
            if not global_singleton.config.has_option(k, o):
                raise Exception("Config file does not contain the required "
                                "option '{}' in section '{}'.".format(o, k))

    loglevel = global_singleton.config.get("LOGGING", "console_log_level")
    try:
        set_logging_level(loglevel)
    except:
        jmprint(
            "Failed to set logging level, must be DEBUG, INFO, WARNING, ERROR",
            "error")

    # Logs to the console are color-coded if user chooses (file is unaffected)
    if global_singleton.config.get("LOGGING", "color") == "true":
        set_logging_color(True)
    else:
        set_logging_color(False)

    try:
        global_singleton.maker_timeout_sec = global_singleton.config.getint(
            'TIMEOUT', 'maker_timeout_sec')
    except NoOptionError:  #pragma: no cover
        log.debug('TIMEOUT/maker_timeout_sec not found in .cfg file, '
                  'using default value')

    # configure the interface to the blockchain on startup
    global_singleton.bc_interface = get_blockchain_interface_instance(
        global_singleton.config)

    #set the location of the commitments file
    try:
        global_singleton.commit_file_location = global_singleton.config.get(
            "POLICY", "commit_file_location")
    except NoOptionError:  #pragma: no cover
        log.debug("No commitment file location in config, using default "
                  "location cmtdata/commitments.json")
    set_commitment_file(
        os.path.join(config_path, global_singleton.commit_file_location))
Exemple #3
0
def load_program_config(config_path="", bs=None, plugin_services=[]):
    global_singleton.config.readfp(io.StringIO(defaultconfig))
    if not config_path:
        config_path = lookup_appdata_folder(global_singleton.APPNAME)
    # we set the global home directory, but keep the config_path variable
    # for callers of this function:
    global_singleton.datadir = config_path
    jmprint("User data location: " + global_singleton.datadir, "info")
    if not os.path.exists(global_singleton.datadir):
        os.makedirs(global_singleton.datadir)
    # prepare folders for wallets and logs
    if not os.path.exists(os.path.join(global_singleton.datadir, "wallets")):
        os.makedirs(os.path.join(global_singleton.datadir, "wallets"))
    if not os.path.exists(os.path.join(global_singleton.datadir, "logs")):
        os.makedirs(os.path.join(global_singleton.datadir, "logs"))
    if not os.path.exists(os.path.join(global_singleton.datadir, "cmtdata")):
        os.makedirs(os.path.join(global_singleton.datadir, "cmtdata"))
    global_singleton.config_location = os.path.join(
        global_singleton.datadir, global_singleton.config_location)

    remove_unwanted_default_settings(global_singleton.config)
    loadedFiles = global_singleton.config.read([global_singleton.config_location
                                               ])
    #Hack required for electrum; must be able to enforce a different
    #blockchain interface even in default/new load.
    if bs:
        global_singleton.config.set("BLOCKCHAIN", "blockchain_source", bs)
    # Create default config file if not found
    if len(loadedFiles) != 1:
        with open(global_singleton.config_location, "w") as configfile:
            configfile.write(defaultconfig)
        jmprint("Created a new `joinmarket.cfg`. Please review and adopt the "
              "settings and restart joinmarket.", "info")
        sys.exit(EXIT_FAILURE)

    #These are left as sanity checks but currently impossible
    #since any edits are overlays to the default, these sections/options will
    #always exist.
    # FIXME: This check is a best-effort attempt. Certain incorrect section
    # names can pass and so can non-first invalid sections.
    for s in required_options: #pragma: no cover
        # check for sections
        avail = None
        if not global_singleton.config.has_section(s):
            for avail in global_singleton.config.sections():
                if avail.startswith(s):
                    break
            else:
                raise Exception(
                    "Config file does not contain the required section: " + s)
        # then check for specific options
        k = avail or s
        for o in required_options[s]:
            if not global_singleton.config.has_option(k, o):
                raise Exception("Config file does not contain the required "
                                "option '{}' in section '{}'.".format(o, k))

    loglevel = global_singleton.config.get("LOGGING", "console_log_level")
    try:
        set_logging_level(loglevel)
    except:
        jmprint("Failed to set logging level, must be DEBUG, INFO, WARNING, ERROR",
                "error")

    # Logs to the console are color-coded if user chooses (file is unaffected)
    if global_singleton.config.get("LOGGING", "color") == "true":
        set_logging_color(True)
    else:
        set_logging_color(False)

    try:
        global_singleton.maker_timeout_sec = global_singleton.config.getint(
            'TIMEOUT', 'maker_timeout_sec')
    except NoOptionError: #pragma: no cover
        log.debug('TIMEOUT/maker_timeout_sec not found in .cfg file, '
                  'using default value')

    # configure the interface to the blockchain on startup
    global_singleton.bc_interface = get_blockchain_interface_instance(
        global_singleton.config)

    # set the location of the commitments file; for non-mainnet a different
    # file is used to avoid conflict
    try:
        global_singleton.commit_file_location = global_singleton.config.get(
            "POLICY", "commit_file_location")
    except NoOptionError: #pragma: no cover
        if get_network() == "mainnet":
            log.debug("No commitment file location in config, using default "
                  "location cmtdata/commitments.json")
    if get_network() != "mainnet":
        # no need to be flexible for tests; note this is used
        # for regtest, signet and testnet3
        global_singleton.commit_file_location = "cmtdata/" + get_network() + \
            "_commitments.json"
    set_commitment_file(os.path.join(config_path,
                                         global_singleton.commit_file_location))

    for p in plugin_services:
        # for now, at this config level, the only significance
        # of a "plugin" is that it keeps its own separate log.
        # We require that a section exists in the config file,
        # and that it has enabled=true:
        assert isinstance(p, JMPluginService)
        if not (global_singleton.config.has_section(p.name) and \
                global_singleton.config.has_option(p.name, "enabled") and \
                global_singleton.config.get(p.name, "enabled") == "true"):
            break
        if p.requires_logging:
            # make sure the environment can accept a logfile by
            # creating the directory in the correct place,
            # and setting that in the plugin object; the plugin
            # itself will switch on its own logging when ready,
            # attaching a filehandler to the global log.
            plogsdir = os.path.join(os.path.dirname(
                global_singleton.config_location), "logs", p.name)
            if not os.path.exists(plogsdir):
                os.makedirs(plogsdir)
            p.set_log_dir(plogsdir)
Exemple #4
0
def main():
    parser = get_sendpayment_parser()
    (options, args) = parser.parse_args()
    load_program_config()
    if options.p2ep and len(args) != 3:
        parser.error("PayJoin requires exactly three arguments: "
                     "wallet, amount and destination address.")
        sys.exit(0)
    elif options.schedule == '' and len(args) != 3:
        parser.error("Joinmarket sendpayment (coinjoin) needs arguments:"
        " wallet, amount and destination address")
        sys.exit(0)

    #without schedule file option, use the arguments to create a schedule
    #of a single transaction
    sweeping = False
    if options.schedule == '':
        #note that sendpayment doesn't support fractional amounts, fractions throw
        #here.
        amount = int(args[1])
        if amount == 0:
            sweeping = True
        destaddr = args[2]
        mixdepth = options.mixdepth
        addr_valid, errormsg = validate_address(destaddr)
        if not addr_valid:
            jmprint('ERROR: Address invalid. ' + errormsg, "error")
            return
        schedule = [[options.mixdepth, amount, options.makercount,
                     destaddr, 0.0, 0]]
    else:
        if options.p2ep:
            parser.error("Schedule files are not compatible with PayJoin")
            sys.exit(0)
        result, schedule = get_schedule(options.schedule)
        if not result:
            log.error("Failed to load schedule file, quitting. Check the syntax.")
            log.error("Error was: " + str(schedule))
            sys.exit(0)
        mixdepth = 0
        for s in schedule:
            if s[1] == 0:
                sweeping = True
            #only used for checking the maximum mixdepth required
            mixdepth = max([mixdepth, s[0]])

    wallet_name = args[0]

    check_regtest()

    if options.pickorders:
        chooseOrdersFunc = pick_order
        if sweeping:
            jmprint('WARNING: You may have to pick offers multiple times', "warning")
            jmprint('WARNING: due to manual offer picking while sweeping', "warning")
    else:
        chooseOrdersFunc = options.order_choose_fn

    # Dynamically estimate a realistic fee if it currently is the default value.
    # At this point we do not know even the number of our own inputs, so
    # we guess conservatively with 2 inputs and 2 outputs each.
    if options.txfee == -1:
        options.txfee = max(options.txfee, estimate_tx_fee(2, 2,
                                        txtype="p2sh-p2wpkh"))
        log.debug("Estimated miner/tx fee for each cj participant: " + str(
            options.txfee))
    assert (options.txfee >= 0)

    maxcjfee = (1, float('inf'))
    if not options.p2ep and not options.pickorders and options.makercount != 0:
        maxcjfee = get_max_cj_fee_values(jm_single().config, options)
        log.info("Using maximum coinjoin fee limits per maker of {:.4%}, {} "
                 "sat".format(*maxcjfee))

    log.debug('starting sendpayment')

    max_mix_depth = max([mixdepth, options.amtmixdepths - 1])

    wallet_path = get_wallet_path(wallet_name, None)
    wallet = open_test_wallet_maybe(
        wallet_path, wallet_name, max_mix_depth, gap_limit=options.gaplimit)

    if jm_single().config.get("BLOCKCHAIN",
        "blockchain_source") == "electrum-server" and options.makercount != 0:
        jm_single().bc_interface.synctype = "with-script"
    #wallet sync will now only occur on reactor start if we're joining.
    while not jm_single().bc_interface.wallet_synced:
        sync_wallet(wallet, fast=options.fastsync)

    # From the estimated tx fees, check if the expected amount is a
    # significant value compared the the cj amount
    total_cj_amount = amount
    if total_cj_amount == 0:
        total_cj_amount = wallet.get_balance_by_mixdepth()[options.mixdepth]
        if total_cj_amount == 0:
            raise ValueError("No confirmed coins in the selected mixdepth. Quitting")
    exp_tx_fees_ratio = ((1 + options.makercount) * options.txfee) / total_cj_amount
    if exp_tx_fees_ratio > 0.05:
        jmprint('WARNING: Expected bitcoin network miner fees for this coinjoin'
            ' amount are roughly {:.1%}'.format(exp_tx_fees_ratio), "warning")
        if input('You might want to modify your tx_fee'
            ' settings in joinmarket.cfg. Still continue? (y/n):')[0] != 'y':
            sys.exit('Aborted by user.')
    else:
        log.info("Estimated miner/tx fees for this coinjoin amount: {:.1%}"
            .format(exp_tx_fees_ratio))

    if options.makercount == 0 and not options.p2ep:
        direct_send(wallet, amount, mixdepth, destaddr, options.answeryes)
        return

    if wallet.get_txtype() == 'p2pkh':
        jmprint("Only direct sends (use -N 0) are supported for "
              "legacy (non-segwit) wallets.", "error")
        return

    def filter_orders_callback(orders_fees, cjamount):
        orders, total_cj_fee = orders_fees
        log.info("Chose these orders: " +pprint.pformat(orders))
        log.info('total cj fee = ' + str(total_cj_fee))
        total_fee_pc = 1.0 * total_cj_fee / cjamount
        log.info('total coinjoin fee = ' + str(float('%.3g' % (
            100.0 * total_fee_pc))) + '%')
        WARNING_THRESHOLD = 0.02  # 2%
        if total_fee_pc > WARNING_THRESHOLD:
            log.info('\n'.join(['=' * 60] * 3))
            log.info('WARNING   ' * 6)
            log.info('\n'.join(['=' * 60] * 1))
            log.info('OFFERED COINJOIN FEE IS UNUSUALLY HIGH. DOUBLE/TRIPLE CHECK.')
            log.info('\n'.join(['=' * 60] * 1))
            log.info('WARNING   ' * 6)
            log.info('\n'.join(['=' * 60] * 3))
        if not options.answeryes:
            if input('send with these orders? (y/n):')[0] != 'y':
                return False
        return True

    def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None):
        if fromtx == "unconfirmed":
            #If final entry, stop *here*, don't wait for confirmation
            if taker.schedule_index + 1 == len(taker.schedule):
                reactor.stop()
            return
        if fromtx:
            if res:
                txd, txid = txdetails
                taker.wallet.remove_old_utxos(txd)
                taker.wallet.add_new_utxos(txd, txid)
                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 taker.latest_tx is None:
                    #can only happen with < minimum_makers; see above.
                    log.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?
                taker.add_honest_makers(list(set(
                    taker.maker_utxo_data.keys()).symmetric_difference(
                        set(taker.nonrespondants))))
                if len(taker.honest_makers) < jm_single().config.getint(
                    "POLICY", "minimum_makers"):
                    log.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(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.
                taker.schedule[taker.schedule_index][2] = len(taker.honest_makers)
                log.info("Retrying with: " + str(taker.schedule[
                    taker.schedule_index][2]) + " counterparties.")
                #rewind to try again (index is incremented in Taker.initialize())
                taker.schedule_index -= 1
                taker.set_honest_only(True)
                reactor.callLater(5.0, clientfactory.getClient().clientStart)
        else:
            if not res:
                log.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:
                log.info("All transactions completed correctly")
            reactor.stop()

    if options.p2ep:
        # This workflow requires command line reading; we force info level logging
        # to remove noise, and mostly communicate to the user with the fn
        # log.info (directly or via default taker_info_callback).
        set_logging_level("INFO")
        # in the case where the payment just hangs for a long period, allow
        # it to fail gracefully with an information message; this is triggered
        # only by the stallMonitor, which gives up after 20*maker_timeout_sec:
        def p2ep_on_finished_callback(res, fromtx=False, waittime=0.0,
                                      txdetails=None):
            log.error("PayJoin payment was NOT made, timed out.")
            reactor.stop()
        taker = P2EPTaker(options.p2ep, wallet, schedule,
                          callbacks=(None, None, p2ep_on_finished_callback))
    else:
        taker = Taker(wallet,
                      schedule,
                      order_chooser=chooseOrdersFunc,
                      max_cj_fee=maxcjfee,
                      callbacks=(filter_orders_callback, None, taker_finished))
    clientfactory = JMClientProtocolFactory(taker)
    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 else False
    p2ep = True if options.p2ep != "" else False
    if jm_single().config.get("BLOCKCHAIN", "network") in ["regtest", "testnet"]:
        startLogging(sys.stdout)
    start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                  jm_single().config.getint("DAEMON", "daemon_port"),
                  clientfactory, daemon=daemon, p2ep=p2ep)
Exemple #5
0
def main():
    parser = get_sendpayment_parser()
    (options, args) = parser.parse_args()
    load_program_config(config_path=options.datadir)
    if options.p2ep and len(args) != 3:
        parser.error("Joinmarket peer-to-peer PayJoin requires exactly three "
                     "arguments: wallet, amount and destination address.")
        sys.exit(EXIT_ARGERROR)
    elif options.schedule == '':
        if ((len(args) < 2) or
            (btc.is_bip21_uri(args[1]) and len(args) != 2) or
            (not btc.is_bip21_uri(args[1]) and len(args) != 3)):
                parser.error("Joinmarket sendpayment (coinjoin) needs arguments:"
                    " wallet, amount, destination address or wallet, bitcoin_uri.")
                sys.exit(EXIT_ARGERROR)

    #without schedule file option, use the arguments to create a schedule
    #of a single transaction
    sweeping = False
    bip78url = None
    if options.schedule == '':
        if btc.is_bip21_uri(args[1]):
            parsed = btc.decode_bip21_uri(args[1])
            try:
                amount = parsed['amount']
            except KeyError:
                parser.error("Given BIP21 URI does not contain amount.")
                sys.exit(EXIT_ARGERROR)
            destaddr = parsed['address']
            if 'jmnick' in parsed:
                if "pj" in parsed:
                    parser.error("Cannot specify both BIP78 and Joinmarket "
                                 "peer-to-peer payjoin at the same time!")
                    sys.exit(EXIT_ARGERROR)
                options.p2ep = parsed['jmnick']
            elif "pj" in parsed:
                # note that this is a URL; its validity
                # checking is deferred to twisted.web.client.Agent
                bip78url = parsed["pj"]
                # setting makercount only for fee sanity check.
                # note we ignore any user setting and enforce N=0,
                # as this is a flag in the code for a non-JM coinjoin;
                # for the fee sanity check, note that BIP78 currently
                # will only allow small fee changes, so N=0 won't
                # be very inaccurate.
                jmprint("Attempting to pay via payjoin.", "info")
                options.makercount = 0
        else:
            amount = btc.amount_to_sat(args[1])
            if amount == 0:
                sweeping = True
            destaddr = args[2]
        mixdepth = options.mixdepth
        addr_valid, errormsg = validate_address(destaddr)
        command_to_burn = (is_burn_destination(destaddr) and sweeping and
            options.makercount == 0 and not options.p2ep)
        if not addr_valid and not command_to_burn:
            jmprint('ERROR: Address invalid. ' + errormsg, "error")
            if is_burn_destination(destaddr):
                jmprint("The required options for burning coins are zero makers"
                    + " (-N 0), sweeping (amount = 0) and not using P2EP", "info")
            sys.exit(EXIT_ARGERROR)
        if sweeping == False and amount < DUST_THRESHOLD:
            jmprint('ERROR: Amount ' + btc.amount_to_str(amount) +
                ' is below dust threshold ' +
                btc.amount_to_str(DUST_THRESHOLD) + '.', "error")
            sys.exit(EXIT_ARGERROR)
        if (options.makercount != 0 and
            options.makercount < jm_single().config.getint(
            "POLICY", "minimum_makers")):
            jmprint('ERROR: Maker count ' + str(options.makercount) +
                ' below minimum_makers (' + str(jm_single().config.getint(
                "POLICY", "minimum_makers")) + ') in joinmarket.cfg.',
                "error")
            sys.exit(EXIT_ARGERROR)
        schedule = [[options.mixdepth, amount, options.makercount,
                     destaddr, 0.0, NO_ROUNDING, 0]]
    else:
        if btc.is_bip21_uri(args[1]):
            parser.error("Schedule files are not compatible with bip21 uris.")
            sys.exit(EXIT_ARGERROR)
        if options.p2ep:
            parser.error("Schedule files are not compatible with PayJoin")
            sys.exit(EXIT_FAILURE)
        result, schedule = get_schedule(options.schedule)
        if not result:
            log.error("Failed to load schedule file, quitting. Check the syntax.")
            log.error("Error was: " + str(schedule))
            sys.exit(EXIT_FAILURE)
        mixdepth = 0
        for s in schedule:
            if s[1] == 0:
                sweeping = True
            #only used for checking the maximum mixdepth required
            mixdepth = max([mixdepth, s[0]])

    wallet_name = args[0]

    check_regtest()

    if options.pickorders:
        chooseOrdersFunc = pick_order
        if sweeping:
            jmprint('WARNING: You may have to pick offers multiple times', "warning")
            jmprint('WARNING: due to manual offer picking while sweeping', "warning")
    else:
        chooseOrdersFunc = options.order_choose_fn

    # If tx_fees are set manually by CLI argument, override joinmarket.cfg:
    if int(options.txfee) > 0:
        jm_single().config.set("POLICY", "tx_fees", str(options.txfee))

    # Dynamically estimate a realistic fee.
    # At this point we do not know even the number of our own inputs, so
    # we guess conservatively with 2 inputs and 2 outputs each.
    fee_per_cp_guess = estimate_tx_fee(2, 2, txtype="p2sh-p2wpkh")
    log.debug("Estimated miner/tx fee for each cj participant: " + str(
        fee_per_cp_guess))

    maxcjfee = (1, float('inf'))
    if not options.p2ep and not options.pickorders and \
       options.makercount != 0:
        maxcjfee = get_max_cj_fee_values(jm_single().config, options)
        log.info("Using maximum coinjoin fee limits per maker of {:.4%}, {} "
                 "".format(maxcjfee[0], btc.amount_to_str(maxcjfee[1])))

    log.info('starting sendpayment')

    max_mix_depth = max([mixdepth, options.amtmixdepths - 1])

    wallet_path = get_wallet_path(wallet_name, None)
    wallet = open_test_wallet_maybe(
        wallet_path, wallet_name, max_mix_depth,
        wallet_password_stdin=options.wallet_password_stdin,
        gap_limit=options.gaplimit)
    wallet_service = WalletService(wallet)
    # in this script, we need the wallet synced before
    # logic processing for some paths, so do it now:
    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=not options.recoversync)
    # the sync call here will now be a no-op:
    wallet_service.startService()


    # From the estimated tx fees, check if the expected amount is a
    # significant value compared the the cj amount; currently enabled
    # only for single join (the predominant, non-advanced case)
    if options.schedule == '':
        total_cj_amount = amount
        if total_cj_amount == 0:
            total_cj_amount = wallet_service.get_balance_by_mixdepth()[options.mixdepth]
            if total_cj_amount == 0:
                raise ValueError("No confirmed coins in the selected mixdepth. Quitting")
        exp_tx_fees_ratio = ((1 + options.makercount) * fee_per_cp_guess) / total_cj_amount
        if exp_tx_fees_ratio > 0.05:
            jmprint('WARNING: Expected bitcoin network miner fees for this coinjoin'
                ' amount are roughly {:.1%}'.format(exp_tx_fees_ratio), "warning")
            if input('You might want to modify your tx_fee'
                ' settings in joinmarket.cfg. Still continue? (y/n):')[0] != 'y':
                sys.exit('Aborted by user.')
        else:
            log.info("Estimated miner/tx fees for this coinjoin amount: {:.1%}"
                .format(exp_tx_fees_ratio))

    if options.makercount == 0 and not options.p2ep and not bip78url:
        tx = direct_send(wallet_service, amount, mixdepth, destaddr,
                         options.answeryes, with_final_psbt=options.with_psbt)
        if options.with_psbt:
            log.info("This PSBT is fully signed and can be sent externally for "
                     "broadcasting:")
            log.info(tx.to_base64())
        return

    if wallet.get_txtype() == 'p2pkh':
        jmprint("Only direct sends (use -N 0) are supported for "
              "legacy (non-segwit) wallets.", "error")
        sys.exit(EXIT_ARGERROR)

    def filter_orders_callback(orders_fees, cjamount):
        orders, total_cj_fee = orders_fees
        log.info("Chose these orders: " +pprint.pformat(orders))
        log.info('total cj fee = ' + str(total_cj_fee))
        total_fee_pc = 1.0 * total_cj_fee / cjamount
        log.info('total coinjoin fee = ' + str(float('%.3g' % (
            100.0 * total_fee_pc))) + '%')
        WARNING_THRESHOLD = 0.02  # 2%
        if total_fee_pc > WARNING_THRESHOLD:
            log.info('\n'.join(['=' * 60] * 3))
            log.info('WARNING   ' * 6)
            log.info('\n'.join(['=' * 60] * 1))
            log.info('OFFERED COINJOIN FEE IS UNUSUALLY HIGH. DOUBLE/TRIPLE CHECK.')
            log.info('\n'.join(['=' * 60] * 1))
            log.info('WARNING   ' * 6)
            log.info('\n'.join(['=' * 60] * 3))
        if not options.answeryes:
            if input('send with these orders? (y/n):')[0] != 'y':
                return False
        return True

    def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None):
        if fromtx == "unconfirmed":
            #If final entry, stop *here*, don't wait for confirmation
            if taker.schedule_index + 1 == len(taker.schedule):
                reactor.stop()
            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 taker.latest_tx is None:
                    #can only happen with < minimum_makers; see above.
                    log.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?
                taker.add_honest_makers(list(set(
                    taker.maker_utxo_data.keys()).symmetric_difference(
                        set(taker.nonrespondants))))
                if len(taker.honest_makers) < jm_single().config.getint(
                    "POLICY", "minimum_makers"):
                    log.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(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.
                taker.schedule[taker.schedule_index][2] = len(taker.honest_makers)
                log.info("Retrying with: " + str(taker.schedule[
                    taker.schedule_index][2]) + " counterparties.")
                #rewind to try again (index is incremented in Taker.initialize())
                taker.schedule_index -= 1
                taker.set_honest_only(True)
                reactor.callLater(5.0, clientfactory.getClient().clientStart)
        else:
            if not res:
                log.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:
                log.info("All transactions completed correctly")
            reactor.stop()

    if options.p2ep:
        # This workflow requires command line reading; we force info level logging
        # to remove noise, and mostly communicate to the user with the fn
        # log.info (directly or via default taker_info_callback).
        set_logging_level("INFO")
        # in the case where the payment just hangs for a long period, allow
        # it to fail gracefully with an information message; this is triggered
        # only by the stallMonitor, which gives up after 20*maker_timeout_sec:
        def p2ep_on_finished_callback(res, fromtx=False, waittime=0.0,
                                      txdetails=None):
            log.error("PayJoin payment was NOT made, timed out.")
            reactor.stop()
        taker = P2EPTaker(options.p2ep, wallet_service, schedule,
                          callbacks=(None, None, p2ep_on_finished_callback))

    elif bip78url:
        # TODO sanity check wallet type is segwit
        manager = parse_payjoin_setup(args[1], wallet_service, options.mixdepth)
        reactor.callWhenRunning(send_payjoin, manager)
        reactor.run()
        return

    else:
        taker = Taker(wallet_service,
                      schedule,
                      order_chooser=chooseOrdersFunc,
                      max_cj_fee=maxcjfee,
                      callbacks=(filter_orders_callback, None, taker_finished))
    clientfactory = JMClientProtocolFactory(taker)
    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 else False
    p2ep = True if options.p2ep != "" else False
    if jm_single().config.get("BLOCKCHAIN", "network") in ["regtest", "testnet"]:
        startLogging(sys.stdout)
    start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                  jm_single().config.getint("DAEMON", "daemon_port"),
                  clientfactory, daemon=daemon, p2ep=p2ep)