Exemple #1
0
    def startService(self):
        """ We instantiate the Maker class only
        here as its constructor will automatically
        create orders based on the wallet.
        Note makers already intrinsically handle
        not-yet-synced wallet services, so there is
        no need to check this here.
        """
        for setup in self.setup_fns:
            # we do not catch Exceptions in setup,
            # deliberately; this must be caught and distinguished
            # by whoever started the service.
            setup()

        # TODO genericise to any YG class:
        self.yieldgen = YieldGeneratorBasic(self.wallet_service,
                                            self.yg_config)
        self.clientfactory = JMClientProtocolFactory(self.yieldgen,
                                                     proto_type="MAKER")
        # here 'start_reactor' does not start the reactor but instantiates
        # the connection to the daemon backend; note daemon=False, i.e. the daemon
        # backend is assumed to be started elsewhere; we just connect to it with a client.
        start_reactor(self.daemon_host,
                      self.daemon_port,
                      self.clientfactory,
                      rs=False)
        # monitor the Maker object, just to check if it's still in an "up" state, marked
        # by the aborted instance var:
        self.monitor_loop = task.LoopingCall(self.monitor)
        self.monitor_loop.start(0.5)
        super().startService()
Exemple #2
0
def test_start_ygs(setup_ygrunner, num_ygs, wallet_structures, mean_amt,
                   malicious):
    """Set up some wallets, for the ygs and 1 sp.
    Then start the ygs in background and publish
    the seed of the sp wallet for easy import into -qt
    """
    wallets = make_wallets(num_ygs + 1,
                           wallet_structures=wallet_structures,
                           mean_amt=mean_amt)
    #the sendpayment bot uses the last wallet in the list
    wallet = wallets[num_ygs]['wallet']
    print("Seed : " + wallets[num_ygs]['seed'])
    #useful to see the utxos on screen sometimes
    sync_wallet(wallet, fast=True)
    print(wallet.unspent)
    txfee = 1000
    cjfee_a = 4200
    cjfee_r = '0.001'
    ordertype = 'swreloffer'
    minsize = 100000
    ygclass = MaliciousYieldGenerator if malicious else YieldGeneratorBasic
    for i in range(num_ygs):
        
        cfg = [txfee, cjfee_a, cjfee_r, ordertype, minsize]
        sync_wallet(wallets[i]["wallet"], fast=True)
        yg = ygclass(wallets[i]["wallet"], cfg)
        if malicious:
            yg.set_maliciousness(malicious)
        clientfactory = JMClientProtocolFactory(yg, proto_type="MAKER")
        nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
        daemon = True if nodaemon == 1 else False
        rs = True if i == num_ygs - 1 else False
        start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                      jm_single().config.getint("DAEMON", "daemon_port"),
                      clientfactory, daemon=daemon, rs=rs)
def test_start_ygs(setup_ygrunner, num_ygs, wallet_structures, mean_amt,
                   malicious, deterministic):
    """Set up some wallets, for the ygs and 1 sp.
    Then start the ygs in background and publish
    the seed of the sp wallet for easy import into -qt
    """
    if jm_single().config.get("POLICY", "native") == "true":
        walletclass = SegwitWallet
    else:
        # TODO add Legacy
        walletclass = SegwitLegacyWallet

    wallet_services = make_wallets(num_ygs + 1,
                                   wallet_structures=wallet_structures,
                                   mean_amt=mean_amt,
                                   walletclass=walletclass)
    #the sendpayment bot uses the last wallet in the list
    wallet_service = wallet_services[num_ygs]['wallet']
    jmprint("\n\nTaker wallet seed : " + wallet_services[num_ygs]['seed'])
    # for manual audit if necessary, show the maker's wallet seeds
    # also (note this audit should be automated in future, see
    # test_full_coinjoin.py in this directory)
    jmprint("\n\nMaker wallet seeds: ")
    for i in range(num_ygs):
        jmprint("Maker seed: " + wallet_services[i]['seed'])
    jmprint("\n")
    wallet_service.sync_wallet(fast=True)
    txfee = 1000
    cjfee_a = 4200
    cjfee_r = '0.001'
    ordertype = 'swreloffer'
    minsize = 100000
    ygclass = YieldGeneratorBasic
    if malicious:
        if deterministic:
            ygclass = DeterministicMaliciousYieldGenerator
        else:
            ygclass = MaliciousYieldGenerator
    for i in range(num_ygs):

        cfg = [txfee, cjfee_a, cjfee_r, ordertype, minsize]
        wallet_service_yg = wallet_services[i]["wallet"]
        wallet_service_yg.startService()
        yg = ygclass(wallet_service_yg, cfg)
        if malicious:
            yg.set_maliciousness(malicious, mtype="tx")
        clientfactory = JMClientProtocolFactory(yg, proto_type="MAKER")
        nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
        daemon = True if nodaemon == 1 else False
        rs = True if i == num_ygs - 1 else False
        start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                      jm_single().config.getint("DAEMON", "daemon_port"),
                      clientfactory,
                      daemon=daemon,
                      rs=rs)
Exemple #4
0
def directory_node_startup():
    parser = OptionParser(usage='usage: %prog [options]')
    add_base_options(parser)
    (options, args) = parser.parse_args()
    # for string access, convert to dict:
    options = vars(options)
    if len(args) != 1:
        parser.error(
            'One argument required: string to be published in the MOTD of the directory node.'
        )
        sys.exit(EXIT_ARGERROR)
    operator_message = args[0]
    # It's possible to set `no-blockchain` in the config file, but this just
    # makes it easier for the user:
    load_program_config(config_path=options["datadir"], bs="no-blockchain")
    # note: you *must* only have the onionmc, no IRC, for this to work,
    # and of course you must only have your own d-node configured here:
    mchan_config = get_mchannels()[0]
    node_location = mchan_config["directory_nodes"]
    # before starting, patch the server handshake default to include our MOTD customization
    # default acceptance false; code must switch it on:
    jmdaemon.onionmc.server_handshake_json[
        "motd"] = "DIRECTORY NODE: {}\nJOINMARKET VERSION: {}\n{}".format(
            node_location, JM_CORE_VERSION, operator_message)
    maker = DNMaker()
    jlog.info('starting directory node')
    clientfactory = DNJMClientProtocolFactory(maker, proto_type="MAKER")
    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = bool(nodaemon)
    if jm_single().config.get("BLOCKCHAIN",
                              "network") in ["regtest", "testnet", "signet"]:
        startLogging(sys.stdout)
    start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                  jm_single().config.getint("DAEMON", "daemon_port"),
                  clientfactory,
                  daemon=daemon)
 def test_do_coinjoin(self):
     """ This slightly weird test curently only
     tests *requesting* a coinjoin; because there are
     no makers running in the test suite, the Taker will
     give up early due to the empty orderbook, but that is
     OK since this API call only makes the request.
     """
     self.daemon.auth_disabled = True
     # in normal operations, the RPC call will trigger
     # the jmclient to connect to an *existing* daemon
     # that was created on startup, but here, that daemon
     # does not yet exist, so we will get 503 Backend Not Ready,
     # unless we manually create it:
     scon, ccon = start_reactor(
         jm_single().config.get("DAEMON", "daemon_host"),
         jm_single().config.getint("DAEMON", "daemon_port"),
         None,
         daemon=True,
         rs=False)
     # must be manually set:
     self.scon = scon
     agent = get_nontor_agent()
     addr = self.get_route_root()
     addr += "/wallet/"
     addr += self.daemon.wallet_name
     addr += "/taker/coinjoin"
     addr = addr.encode()
     body = BytesProducer(
         json.dumps({
             "mixdepth": "1",
             "amount_sats": "22000000",
             "counterparties": "2",
             "destination": "2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br"
         }).encode())
     yield self.do_request(agent, b"POST", addr, body,
                           self.process_do_coinjoin_response)
Exemple #6
0
def ygmain(ygclass,
           txfee=1000,
           cjfee_a=200,
           cjfee_r=0.002,
           ordertype='swreloffer',
           nickserv_password='',
           minsize=100000,
           gaplimit=6):
    import sys

    parser = OptionParser(usage='usage: %prog [options] [wallet file]')
    add_base_options(parser)
    parser.add_option('-o',
                      '--ordertype',
                      action='store',
                      type='string',
                      dest='ordertype',
                      default=ordertype,
                      help='type of order; can be either reloffer or absoffer')
    parser.add_option('-t',
                      '--txfee',
                      action='store',
                      type='int',
                      dest='txfee',
                      default=txfee,
                      help='minimum miner fee in satoshis')
    parser.add_option('-c',
                      '--cjfee',
                      action='store',
                      type='string',
                      dest='cjfee',
                      default='',
                      help='requested coinjoin fee in satoshis or proportion')
    parser.add_option('-p',
                      '--password',
                      action='store',
                      type='string',
                      dest='password',
                      default=nickserv_password,
                      help='irc nickserv password')
    parser.add_option('-s',
                      '--minsize',
                      action='store',
                      type='int',
                      dest='minsize',
                      default=minsize,
                      help='minimum coinjoin size in satoshis')
    parser.add_option('-g',
                      '--gap-limit',
                      action='store',
                      type="int",
                      dest='gaplimit',
                      default=gaplimit,
                      help='gap limit for wallet, default=' + str(gaplimit))
    parser.add_option('-m',
                      '--mixdepth',
                      action='store',
                      type='int',
                      dest='mixdepth',
                      default=None,
                      help="highest mixdepth to use")
    (options, args) = parser.parse_args()
    if len(args) < 1:
        parser.error('Needs a wallet')
        sys.exit(EXIT_ARGERROR)
    wallet_name = args[0]
    ordertype = options.ordertype
    txfee = options.txfee
    if ordertype in ('reloffer', 'swreloffer'):
        if options.cjfee != '':
            cjfee_r = options.cjfee
        # minimum size is such that you always net profit at least 20%
        #of the miner fee
        minsize = max(int(1.2 * txfee / float(cjfee_r)), options.minsize)
    elif ordertype in ('absoffer', 'swabsoffer'):
        if options.cjfee != '':
            cjfee_a = int(options.cjfee)
        minsize = options.minsize
    else:
        parser.error('You specified an incorrect offer type which ' +\
                     'can be either swreloffer or swabsoffer')
        sys.exit(EXIT_ARGERROR)
    nickserv_password = options.password

    load_program_config(config_path=options.datadir)

    if jm_single().bc_interface is None:
        jlog.error("Running yield generator requires configured " +
                   "blockchain source.")
        sys.exit(EXIT_FAILURE)

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

    wallet_service = WalletService(wallet)
    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=not options.recoversync)
    wallet_service.startService()

    maker = ygclass(
        wallet_service,
        [options.txfee, cjfee_a, cjfee_r, options.ordertype, options.minsize])
    jlog.info('starting yield generator')
    clientfactory = JMClientProtocolFactory(maker, proto_type="MAKER")

    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 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)
Exemple #7
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 #8
0
def main():
    parser = get_sendpayment_parser()
    (options, args) = parser.parse_args()
    load_program_config(config_path=options.datadir)

    if 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 "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)
        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 BIP78 Payjoin",
                    "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 len(args) > 1:
            parser.error("Schedule files are not compatible with "
                         "payment destination/amount arguments.")
            sys.exit(EXIT_ARGERROR)
        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))

    maxcjfee = (1, float('inf'))
    if 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)
    if wallet_service.rpc_error:
        sys.exit(EXIT_FAILURE)
    # 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()

    # Dynamically estimate a realistic fee, for coinjoins.
    # 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.makercount != 0:
        fee_per_cp_guess = estimate_tx_fee(2,
                                           2,
                                           txtype=wallet_service.get_txtype())
        log.debug("Estimated miner/tx fee for each cj participant: " +
                  btc.amount_to_str(fee_per_cp_guess))

    # 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 == '' and options.makercount != 0:
        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 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 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
    if jm_single().config.get("BLOCKCHAIN", "network") == "regtest":
        startLogging(sys.stdout)
    start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                  jm_single().config.getint("DAEMON", "daemon_port"),
                  clientfactory,
                  daemon=daemon)
Exemple #9
0
def receive_payjoin_main(makerclass):
    parser = OptionParser(
        usage='usage: %prog [options] [wallet file] [amount-to-receive]')
    parser.add_option('-g',
                      '--gap-limit',
                      action='store',
                      type="int",
                      dest='gaplimit',
                      default=6,
                      help='gap limit for wallet, default=6')
    parser.add_option('--recoversync',
                      action='store_true',
                      dest='recoversync',
                      default=False,
                      help=('choose to do detailed wallet sync, '
                            'used for recovering on new Core instance.'))
    parser.add_option('-m',
                      '--mixdepth',
                      action='store',
                      type='int',
                      dest='mixdepth',
                      default=0,
                      help="mixdepth to source coins from")
    parser.add_option('-a',
                      '--amtmixdepths',
                      action='store',
                      type='int',
                      dest='amtmixdepths',
                      help='number of mixdepths in wallet, default 5',
                      default=5)
    parser.add_option('--wallet-password-stdin',
                      action='store_true',
                      default=False,
                      dest='wallet_password_stdin',
                      help='Read wallet password from stdin')

    (options, args) = parser.parse_args()
    if len(args) < 2:
        parser.error(
            'Needs a wallet, and a receiving amount in bitcoins or satoshis')
        sys.exit(EXIT_ARGERROR)
    wallet_name = args[0]
    try:
        receiving_amount = amount_to_sat(args[1])
    except:
        parser.error("Invalid receiving amount passed: " + receiving_amount)
        sys.exit(EXIT_FAILURE)
    if receiving_amount < 0:
        parser.error("Receiving amount must be a positive number")
        sys.exit(EXIT_FAILURE)
    load_program_config()

    check_regtest()

    # 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 (via P2EPMaker.user_info).
    set_logging_level("INFO")

    wallet_path = get_wallet_path(wallet_name, 'wallets')
    max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1])
    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)

    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=not options.recoversync)
    wallet_service.startService()

    maker = makerclass(wallet_service, options.mixdepth, receiving_amount)

    jlog.info('starting receive-payjoin')
    clientfactory = JMClientProtocolFactory(maker, proto_type="MAKER")

    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 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=True)
Exemple #10
0
def test_start_ygs(setup_ygrunner, num_ygs, wallet_structures, mean_amt,
                   malicious, deterministic):
    """Set up some wallets, for the ygs and 1 sp.
    Then start the ygs in background and publish
    the seed of the sp wallet for easy import into -qt
    """
    if jm_single().config.get("POLICY", "native") == "true":
        walletclass = SegwitWallet
    else:
        # TODO add Legacy
        walletclass = SegwitLegacyWallet

    wallet_services = make_wallets(num_ygs + 1,
                           wallet_structures=wallet_structures,
                           mean_amt=mean_amt,
                           walletclass=walletclass)
    #the sendpayment bot uses the last wallet in the list
    wallet_service = wallet_services[num_ygs]['wallet']
    jmprint("\n\nTaker wallet seed : " + wallet_services[num_ygs]['seed'])
    # for manual audit if necessary, show the maker's wallet seeds
    # also (note this audit should be automated in future, see
    # test_full_coinjoin.py in this directory)
    jmprint("\n\nMaker wallet seeds: ")
    for i in range(num_ygs):
        jmprint("Maker seed: " + wallet_services[i]['seed'])
    jmprint("\n")
    wallet_service.sync_wallet(fast=True)
    ygclass = YieldGeneratorBasic

    # As per previous note, override non-default command line settings:
    options = {}
    for x in ["ordertype", "txfee", "txfee_factor", "cjfee_a", "cjfee_r",
              "cjfee_factor", "minsize", "size_factor"]:
        options[x] = jm_single().config.get("YIELDGENERATOR", x)
    ordertype = options["ordertype"]
    txfee = int(options["txfee"])
    txfee_factor = float(options["txfee_factor"])
    cjfee_factor = float(options["cjfee_factor"])
    size_factor = float(options["size_factor"])
    if ordertype == 'reloffer':
        cjfee_r = options["cjfee_r"]
        # minimum size is such that you always net profit at least 20%
        #of the miner fee
        minsize = max(int(1.2 * txfee / float(cjfee_r)), int(options["minsize"]))
        cjfee_a = None
    elif ordertype == 'absoffer':
        cjfee_a = int(options["cjfee_a"])
        minsize = int(options["minsize"])
        cjfee_r = None
    else:
        assert False, "incorrect offertype config for yieldgenerator."

    txtype = wallet_service.get_txtype()
    if txtype == "p2wpkh":
        prefix = "sw0"
    elif txtype == "p2sh-p2wpkh":
        prefix = "sw"
    elif txtype == "p2pkh":
        prefix = ""
    else:
        assert False, "Unsupported wallet type for yieldgenerator: " + txtype

    ordertype = prefix + ordertype

    if malicious:
        if deterministic:
            ygclass = DeterministicMaliciousYieldGenerator
        else:
            ygclass = MaliciousYieldGenerator
    for i in range(num_ygs):
        
        cfg = [txfee, cjfee_a, cjfee_r, ordertype, minsize, txfee_factor,
               cjfee_factor, size_factor]
        wallet_service_yg = wallet_services[i]["wallet"]
        wallet_service_yg.startService()
        yg = ygclass(wallet_service_yg, cfg)
        if malicious:
            yg.set_maliciousness(malicious, mtype="tx")
        clientfactory = JMClientProtocolFactory(yg, proto_type="MAKER")
        nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
        daemon = True if nodaemon == 1 else False
        rs = True if i == num_ygs - 1 else False
        start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                      jm_single().config.getint("DAEMON", "daemon_port"),
                      clientfactory, daemon=daemon, rs=rs)
def test_cj(setup_full_coinjoin, num_ygs, wallet_structures, mean_amt,
            malicious, deterministic):
    """Starts by setting up wallets for maker and taker bots; then,
    instantiates a single taker with the final wallet.
    The remaining wallets are used to set up YieldGenerators (basic form).
    All the wallets are given coins according to the rules of make_wallets,
    using the parameters for the values.
    The final start_reactor call is the only one that actually starts the
    reactor; the others only set up protocol instances.
    Inline are custom callbacks for the Taker, and these are basically
    copies of those in the `sendpayment.py` script for now, but they could
    be customized later for testing.
    The Taker's schedule is a single coinjoin, using basically random values,
    again this could be easily edited or parametrized if we feel like it.
    """

    # Set up some wallets, for the ygs and 1 sp.
    wallets = make_wallets(num_ygs + 1,
                           wallet_structures=wallet_structures,
                           mean_amt=mean_amt)
    #the sendpayment bot uses the last wallet in the list
    wallet = wallets[num_ygs]['wallet']
    sync_wallet(wallet, fast=True)
    # grab a dest addr from the wallet
    destaddr = wallet.get_external_addr(4)
    coinjoin_amt = 20000000
    schedule = [[1, coinjoin_amt, 2, destaddr, 0.0, False]]
    """ The following two callback functions are as simple as possible
    modifications of the same in scripts/sendpayment.py
    """
    def filter_orders_callback(orders_fees, cjamount):
        return True

    def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None):
        def final_checks():
            sync_wallet(wallet, fast=True)
            newbal = wallet.get_balance_by_mixdepth()[4]
            oldbal = wallet.get_balance_by_mixdepth()[1]
            # These are our check that the coinjoin succeeded
            assert newbal == coinjoin_amt
            # TODO: parametrize these; cj fees = 38K (.001 x 20M x 2 makers)
            # minus 1K tx fee contribution each; 600M is original balance
            # in mixdepth 1
            assert oldbal + newbal + (40000 -
                                      2000) + taker.total_txfee == 600000000

        if fromtx == "unconfirmed":
            #If final entry, stop *here*, don't wait for confirmation
            if taker.schedule_index + 1 == len(taker.schedule):
                reactor.stop()
                final_checks()
                return
        if fromtx:
            # currently this test uses a schedule with only one entry
            assert False, "taker_finished was called with fromtx=True"
            reactor.stop()
            return
        else:
            if not res:
                assert False, "Did not complete successfully, shutting down"
            # Note that this is required in both conditional branches,
            # especially in testing, because it's possible to receive the
            # confirmed callback before the unconfirmed.
            reactor.stop()
            final_checks()

    # twisted logging is required for debugging:
    startLogging(sys.stdout)

    taker = Taker(wallet,
                  schedule,
                  order_chooser=random_under_max_order_choose,
                  max_cj_fee=(0.1, 200),
                  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
    start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                  jm_single().config.getint("DAEMON", "daemon_port"),
                  clientfactory,
                  daemon=daemon,
                  rs=False)

    txfee = 1000
    cjfee_a = 4200
    cjfee_r = '0.001'
    ordertype = 'swreloffer'
    minsize = 100000
    ygclass = YieldGeneratorBasic
    # As noted above, this is not currently used but can be in future:
    if malicious or deterministic:
        raise NotImplementedError
    for i in range(num_ygs):
        cfg = [txfee, cjfee_a, cjfee_r, ordertype, minsize]
        sync_wallet(wallets[i]["wallet"], fast=True)
        yg = ygclass(wallets[i]["wallet"], cfg)
        if malicious:
            yg.set_maliciousness(malicious, mtype="tx")
        clientfactory = JMClientProtocolFactory(yg, proto_type="MAKER")
        nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
        daemon = True if nodaemon == 1 else False
        # As noted above, only the final start_reactor() call will
        # actually start it!
        rs = True if i == num_ygs - 1 else False
        start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                      jm_single().config.getint("DAEMON", "daemon_port"),
                      clientfactory,
                      daemon=daemon,
                      rs=rs)
def receive_payjoin_main(makerclass):
    parser = OptionParser(usage='usage: %prog [options] [wallet file] [amount-to-receive]')
    parser.add_option('-g', '--gap-limit', action='store', type="int",
                      dest='gaplimit', default=6,
                      help='gap limit for wallet, default=6')
    parser.add_option('--fast',
                      action='store_true',
                      dest='fastsync',
                      default=False,
                      help=('choose to do fast wallet sync, only for Core and '
                            'only for previously synced wallet'))
    parser.add_option('-m', '--mixdepth', action='store', type='int',
                      dest='mixdepth', default=0,
                      help="mixdepth to source coins from")
    parser.add_option('-a',
                      '--amtmixdepths',
                      action='store',
                      type='int',
                      dest='amtmixdepths',
                      help='number of mixdepths in wallet, default 5',
                      default=5)    
    (options, args) = parser.parse_args()
    if len(args) < 2:
        parser.error('Needs a wallet, and a receiving amount in satoshis')
        sys.exit(0)
    wallet_name = args[0]
    try:
        receiving_amount = int(args[1])
    except:
        parser.error("Invalid receiving amount passed: " + receiving_amount)
        sys.exit(0)
    if receiving_amount < 0:
        parser.error("Receiving amount must be a positive integer in satoshis")
        sys.exit(0)
    load_program_config()

    check_regtest()

    # 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 (via P2EPMaker.user_info).
    set_logging_level("INFO")

    wallet_path = get_wallet_path(wallet_name, 'wallets')
    max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1])
    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":
        jm_single().bc_interface.synctype = "with-script"

    while not jm_single().bc_interface.wallet_synced:
        sync_wallet(wallet, fast=options.fastsync)

    maker = makerclass(wallet, options.mixdepth, receiving_amount)
    
    jlog.info('starting receive-payjoin')
    clientfactory = JMClientProtocolFactory(maker, proto_type="MAKER")

    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 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=True)
Exemple #13
0
def main():
    tumble_log = get_tumble_log(logsdir)
    (options, args) = get_tumbler_parser().parse_args()
    options = vars(options)
    if len(args) < 1:
        parser.error('Needs a wallet file')
        sys.exit(0)
    load_program_config()
    #Load the wallet
    wallet_name = args[0]
    max_mix_depth = options['mixdepthsrc'] + options['mixdepthcount']
    if not os.path.exists(os.path.join('wallets', wallet_name)):
        wallet = Wallet(wallet_name, None, max_mix_depth)
    else:
        while True:
            try:
                pwd = get_password("Enter wallet decryption passphrase: ")
                wallet = Wallet(wallet_name, pwd, max_mix_depth)
            except WalletError:
                print("Wrong password, try again.")
                continue
            except Exception as e:
                print("Failed to load wallet, error message: " + repr(e))
                sys.exit(0)
            break
    sync_wallet(wallet, fast=options['fastsync'])

    #Parse options and generate schedule
    #Output information to log files
    jm_single().mincjamount = options['mincjamount']
    destaddrs = args[1:]
    print(destaddrs)
    #If the --restart flag is set we read the schedule
    #from the file, and filter out entries that are
    #already complete
    if options['restart']:
        res, schedule = get_schedule(os.path.join(logsdir,
                                                  options['schedulefile']))
        if not res:
            print("Failed to load schedule, name: " + str(
                options['schedulefile']))
            print("Error was: " + str(schedule))
            sys.exit(0)
        #This removes all entries that are marked as done
        schedule = [s for s in schedule if s[5] != 1]
        if isinstance(schedule[0][5], str) and len(schedule[0][5]) == 64:
            #ensure last transaction is confirmed before restart
            tumble_log.info("WAITING TO RESTART...")
            txid = schedule[0][5]
            restart_waiter(txid + ":0") #add 0 index because all have it
            #remove the already-done entry (this connects to the other TODO,
            #probably better *not* to truncate the done-already txs from file,
            #but simplest for now.
            schedule = schedule[1:]
        elif schedule[0][5] != 0:
            print("Error: first schedule entry is invalid.")
            sys.exit(0)
        with open(os.path.join(logsdir, options['schedulefile']), "wb") as f:
                    f.write(schedule_to_text(schedule))
        tumble_log.info("TUMBLE RESTARTING")
    else:
        #Create a new schedule from scratch
        schedule = get_tumble_schedule(options, destaddrs)
        tumble_log.info("TUMBLE STARTING")
        with open(os.path.join(logsdir, options['schedulefile']), "wb") as f:
            f.write(schedule_to_text(schedule))
        print("Schedule written to logs/" + options['schedulefile'])
    tumble_log.info("With this schedule: ")
    tumble_log.info(pprint.pformat(schedule))

    print("Progress logging to logs/TUMBLE.log")

    def filter_orders_callback(orders_fees, cjamount):
        """Decide whether to accept fees
        """
        return tumbler_filter_orders_callback(orders_fees, cjamount, taker, options)

    def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None):
        """on_finished_callback for tumbler; processing is almost entirely
        deferred to generic taker_finished in tumbler_support module, except
        here reactor signalling.
        """
        sfile = os.path.join(logsdir, options['schedulefile'])
        tumbler_taker_finished_update(taker, sfile, tumble_log, options,
                                      res, fromtx, waittime, txdetails)
        if not fromtx:
            reactor.stop()
        elif fromtx != "unconfirmed":
            reactor.callLater(waittime*60, clientfactory.getClient().clientStart)

    #to allow testing of confirm/unconfirm callback for multiple txs
    if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface):
        jm_single().bc_interface.tick_forward_chain_interval = 10
        jm_single().maker_timeout_sec = 5

    #instantiate Taker with given schedule and run
    taker = Taker(wallet,
                  schedule,
                  order_chooser=weighted_order_choose,
                  callbacks=(filter_orders_callback, None, taker_finished),
                  tdestaddrs=destaddrs)
    clientfactory = JMTakerClientProtocolFactory(taker)
    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 else False
    start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                  jm_single().config.getint("DAEMON", "daemon_port"),
                  clientfactory, daemon=daemon)
        def docoinjoin(self, request, walletname):
            self.check_cookie(request)
            if not self.services["wallet"]:
                raise NoWalletFound()
            if not self.wallet_name == walletname:
                raise InvalidRequestFormat()
            request_data = self.get_POST_body(
                request,
                ["mixdepth", "amount_sats", "counterparties", "destination"])
            if not request_data:
                raise InvalidRequestFormat()
            #see file scripts/sample-schedule-for-testnet for schedule format
            waittime = 0
            rounding = 16
            completion_flag = 0
            # A schedule is a list of lists, here we have only one item
            try:
                schedule = [[
                    int(request_data["mixdepth"]),
                    int(request_data["amount_sats"]),
                    int(request_data["counterparties"]),
                    request_data["destination"], waittime, rounding,
                    completion_flag
                ]]
            except ValueError:
                raise InvalidRequestFormat()
            # Instantiate a Taker.
            # `order_chooser` is whatever is default for Taker.
            # max_cj_fee is to be set based on config values.
            # If user has not set config, we only for now raise
            # an error specific to this case; in future we can
            # pass a request to a client to set the values, as
            # we do in CLI (the usual reasoning applies as to
            # why no defaults).
            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)
            # Before actual start, update our coinjoin state:
            if not self.activate_coinjoin_state(CJ_TAKER_RUNNING):
                raise ServiceAlreadyStarted()
            self.taker = Taker(self.services["wallet"],
                               schedule,
                               max_cj_fee=max_cj_fee,
                               callbacks=(self.filter_orders_callback, None,
                                          self.taker_finished))
            # TODO ; this makes use of a pre-existing hack to allow
            # selectively disabling the stallMonitor function that checks
            # if transactions went through or not; here we want to cleanly
            # destroy the Taker after an attempt is made, successful or not.
            self.taker.testflag = True
            self.clientfactory = self.get_client_factory()

            dhost, dport = self.check_daemon_ready()

            _, self.coinjoin_connection = start_reactor(dhost,
                                                        dport,
                                                        self.clientfactory,
                                                        rs=False)
            return make_jmwalletd_response(request, status=202)
def ygmain(ygclass, nickserv_password='', gaplimit=6):
    import sys

    parser = OptionParser(usage='usage: %prog [options] [wallet file]')
    add_base_options(parser)
    # A note about defaults:
    # We want command line settings to override config settings.
    # This would naturally mean setting `default=` arguments here, to the
    # values in the config.
    # However, we cannot load the config until we know the datadir.
    # The datadir is a setting in the command line options, so we have to
    # call parser.parse_args() before we know the datadir.
    # Hence we do the following: set all modifyable-by-config arguments to
    # default "None" initially; call parse_args(); then call load_program_config
    # and override values of "None" with what is set in the config.
    # (remember, the joinmarket defaultconfig always sets every value, even if
    # the user doesn't).
    parser.add_option('-o',
                      '--ordertype',
                      action='store',
                      type='string',
                      dest='ordertype',
                      default=None,
                      help='type of order; can be either reloffer or absoffer')
    parser.add_option('-t',
                      '--txfee',
                      action='store',
                      type='int',
                      dest='txfee',
                      default=None,
                      help='minimum miner fee in satoshis')
    parser.add_option('-f',
                      '--txfee-factor',
                      action='store',
                      type='float',
                      dest='txfee_factor',
                      default=None,
                      help='variance around the average fee, decimal fraction')
    parser.add_option('-a',
                      '--cjfee-a',
                      action='store',
                      type='string',
                      dest='cjfee_a',
                      default=None,
                      help='requested coinjoin fee (absolute) in satoshis')
    parser.add_option('-r',
                      '--cjfee-r',
                      action='store',
                      type='string',
                      dest='cjfee_r',
                      default=None,
                      help='requested coinjoin fee (relative) as a decimal')
    parser.add_option('-j',
                      '--cjfee-factor',
                      action='store',
                      type='float',
                      dest='cjfee_factor',
                      default=None,
                      help='variance around the average fee, decimal fraction')
    parser.add_option('-p',
                      '--password',
                      action='store',
                      type='string',
                      dest='password',
                      default=nickserv_password,
                      help='irc nickserv password')
    parser.add_option('-s',
                      '--minsize',
                      action='store',
                      type='int',
                      dest='minsize',
                      default=None,
                      help='minimum coinjoin size in satoshis')
    parser.add_option('-z',
                      '--size-factor',
                      action='store',
                      type='float',
                      dest='size_factor',
                      default=None,
                      help='variance around all offer sizes, decimal fraction')
    parser.add_option('-g',
                      '--gap-limit',
                      action='store',
                      type="int",
                      dest='gaplimit',
                      default=gaplimit,
                      help='gap limit for wallet, default=' + str(gaplimit))
    parser.add_option('-m',
                      '--mixdepth',
                      action='store',
                      type='int',
                      dest='mixdepth',
                      default=None,
                      help="highest mixdepth to use")
    (options, args) = parser.parse_args()
    # for string access, convert to dict:
    options = vars(options)
    if len(args) < 1:
        parser.error('Needs a wallet')
        sys.exit(EXIT_ARGERROR)

    load_program_config(config_path=options["datadir"])

    # As per previous note, override non-default command line settings:
    for x in [
            "ordertype", "txfee", "txfee_factor", "cjfee_a", "cjfee_r",
            "cjfee_factor", "minsize", "size_factor"
    ]:
        if options[x] is None:
            options[x] = jm_single().config.get("YIELDGENERATOR", x)
    wallet_name = args[0]
    ordertype = options["ordertype"]
    txfee = int(options["txfee"])
    txfee_factor = float(options["txfee_factor"])
    cjfee_factor = float(options["cjfee_factor"])
    size_factor = float(options["size_factor"])
    if ordertype == 'reloffer':
        cjfee_r = options["cjfee_r"]
        # minimum size is such that you always net profit at least 20%
        #of the miner fee
        minsize = max(int(1.2 * txfee / float(cjfee_r)),
                      int(options["minsize"]))
        cjfee_a = None
    elif ordertype == 'absoffer':
        cjfee_a = int(options["cjfee_a"])
        minsize = int(options["minsize"])
        cjfee_r = None
    else:
        parser.error('You specified an incorrect offer type which ' +\
                     'can be either reloffer or absoffer')
        sys.exit(EXIT_ARGERROR)
    nickserv_password = options["password"]

    if jm_single().bc_interface is None:
        jlog.error("Running yield generator requires configured " +
                   "blockchain source.")
        sys.exit(EXIT_FAILURE)

    wallet_path = get_wallet_path(wallet_name, None)
    wallet = open_test_wallet_maybe(
        wallet_path,
        wallet_name,
        options["mixdepth"],
        wallet_password_stdin=options["wallet_password_stdin"],
        gap_limit=options["gaplimit"])

    wallet_service = WalletService(wallet)
    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=not options["recoversync"])
    wallet_service.startService()

    txtype = wallet_service.get_txtype()
    if txtype == "p2wpkh":
        prefix = "sw0"
    elif txtype == "p2sh-p2wpkh":
        prefix = "sw"
    elif txtype == "p2pkh":
        prefix = ""
    else:
        jlog.error("Unsupported wallet type for yieldgenerator: " + txtype)
        sys.exit(EXIT_ARGERROR)

    ordertype = prefix + ordertype
    jlog.debug("Set the offer type string to: " + ordertype)

    maker = ygclass(wallet_service, [
        txfee, cjfee_a, cjfee_r, ordertype, minsize, txfee_factor,
        cjfee_factor, size_factor
    ])
    jlog.info('starting yield generator')
    clientfactory = JMClientProtocolFactory(maker, proto_type="MAKER")
    if jm_single().config.get("SNICKER", "enabled") == "true":
        if jm_single().config.get("BLOCKCHAIN", "network") == "mainnet":
            jlog.error("You have enabled SNICKER on mainnet, this is not "
                       "yet supported for yieldgenerators; either use "
                       "signet/regtest/testnet, or run SNICKER manually "
                       "with snicker/receive-snicker.py.")
            sys.exit(EXIT_ARGERROR)
        snicker_r = SNICKERReceiver(wallet_service)
        servers = jm_single().config.get("SNICKER", "servers").split(",")
        snicker_factory = SNICKERClientProtocolFactory(snicker_r, servers)
    else:
        snicker_factory = None
    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 else False
    if jm_single().config.get("BLOCKCHAIN",
                              "network") in ["regtest", "testnet", "signet"]:
        startLogging(sys.stdout)
    start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                  jm_single().config.getint("DAEMON", "daemon_port"),
                  clientfactory,
                  snickerfactory=snicker_factory,
                  daemon=daemon)
Exemple #16
0
def main():
    parser = get_sendpayment_parser()
    (options, args) = parser.parse_args()
    load_program_config()
    if options.schedule == '' and len(args) < 3:
        parser.error('Needs a 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:
            print('ERROR: Address invalid. ' + errormsg)
            return
        schedule = [[
            options.mixdepth, amount, options.makercount, destaddr, 0.0, 0
        ]]
    else:
        result, schedule = get_schedule(options.schedule)
        if not result:
            log.info(
                "Failed to load schedule file, quitting. Check the syntax.")
            log.info("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]

    #to allow testing of confirm/unconfirm callback for multiple txs
    if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface):
        jm_single().bc_interface.tick_forward_chain_interval = 10
        jm_single().bc_interface.simulating = True
        jm_single().maker_timeout_sec = 15

    chooseOrdersFunc = None
    if options.pickorders:
        chooseOrdersFunc = pick_order
        if sweeping:
            print('WARNING: You may have to pick offers multiple times')
            print('WARNING: due to manual offer picking while sweeping')
    elif options.choosecheapest:
        chooseOrdersFunc = cheapest_order_choose
    else:  # choose randomly (weighted)
        chooseOrdersFunc = weighted_order_choose

    # 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)

    log.debug('starting sendpayment')

    if not options.userpcwallet:
        #maxmixdepth in the wallet is actually the *number* of mixdepths (so misnamed);
        #to ensure we have enough, must be at least (requested index+1)
        max_mix_depth = max([mixdepth + 1, options.amtmixdepths])
        if not os.path.exists(os.path.join('wallets', wallet_name)):
            wallet = get_wallet_cls()(wallet_name, None, max_mix_depth,
                                      options.gaplimit)
        else:
            while True:
                try:
                    pwd = get_password("Enter wallet decryption passphrase: ")
                    wallet = get_wallet_cls()(wallet_name, pwd, max_mix_depth,
                                              options.gaplimit)
                except WalletError:
                    print("Wrong password, try again.")
                    continue
                except Exception as e:
                    print("Failed to load wallet, error message: " + repr(e))
                    sys.exit(0)
                break
    else:
        wallet = BitcoinCoreWallet(fromaccount=wallet_name)
    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.
    sync_wallet(wallet, fast=options.fastsync)
    if options.makercount == 0:
        if isinstance(wallet, BitcoinCoreWallet):
            raise NotImplementedError(
                "Direct send only supported for JM wallets")
        direct_send(wallet, amount, mixdepth, destaddr, options.answeryes)
        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 raw_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; just stop
                reactor.stop()
        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()

    taker = Taker(wallet,
                  schedule,
                  order_chooser=chooseOrdersFunc,
                  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
    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)
Exemple #17
0
def main():
    tumble_log = get_tumble_log(logsdir)
    (options, args) = get_tumbler_parser().parse_args()
    options_org = options
    options = vars(options)
    if len(args) < 1:
        jmprint('Error: Needs a wallet file', "error")
        sys.exit(EXIT_ARGERROR)
    load_program_config(config_path=options['datadir'])

    if jm_single().bc_interface is None:
        jmprint('Error: Needs a blockchain source', "error")
        sys.exit(EXIT_FAILURE)

    check_regtest()

    #Load the wallet
    wallet_name = args[0]
    max_mix_depth = options['mixdepthsrc'] + options['mixdepthcount']
    if options['amtmixdepths'] > max_mix_depth:
        max_mix_depth = options['amtmixdepths']
    wallet_path = get_wallet_path(wallet_name, None)
    wallet = open_test_wallet_maybe(
        wallet_path,
        wallet_name,
        max_mix_depth,
        wallet_password_stdin=options_org.wallet_password_stdin)
    wallet_service = WalletService(wallet)
    if wallet_service.rpc_error:
        sys.exit(EXIT_FAILURE)
    # 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()

    maxcjfee = get_max_cj_fee_values(jm_single().config, options_org)
    log.info(
        "Using maximum coinjoin fee limits per maker of {:.4%}, {} sat".format(
            *maxcjfee))

    #Parse options and generate schedule
    #Output information to log files
    jm_single().mincjamount = options['mincjamount']
    destaddrs = args[1:]
    for daddr in destaddrs:
        success, errmsg = validate_address(daddr)
        if not success:
            jmprint("Invalid destination address: " + daddr, "error")
            sys.exit(EXIT_ARGERROR)
    jmprint("Destination addresses: " + str(destaddrs), "important")
    #If the --restart flag is set we read the schedule
    #from the file, and filter out entries that are
    #already complete
    if options['restart']:
        res, schedule = get_schedule(
            os.path.join(logsdir, options['schedulefile']))
        if not res:
            jmprint(
                "Failed to load schedule, name: " +
                str(options['schedulefile']), "error")
            jmprint("Error was: " + str(schedule), "error")
            sys.exit(EXIT_FAILURE)
        #This removes all entries that are marked as done
        schedule = [s for s in schedule if s[-1] != 1]
        # remaining destination addresses must be stored in Taker.tdestaddrs
        # in case of tweaks; note we can't change, so any passed on command
        # line must be ignored:
        if len(destaddrs) > 0:
            jmprint(
                "For restarts, destinations are taken from schedule file,"
                " so passed destinations on the command line were ignored.",
                "important")
            if input("OK? (y/n)") != "y":
                sys.exit(EXIT_SUCCESS)
        destaddrs = [
            s[3] for s in schedule if s[3] not in ["INTERNAL", "addrask"]
        ]
        jmprint(
            "Remaining destination addresses in restart: " +
            ",".join(destaddrs), "important")
        if isinstance(schedule[0][-1], str) and len(schedule[0][-1]) == 64:
            #ensure last transaction is confirmed before restart
            tumble_log.info("WAITING TO RESTART...")
            txid = schedule[0][-1]
            restart_waiter(txid)
            #remove the already-done entry (this connects to the other TODO,
            #probably better *not* to truncate the done-already txs from file,
            #but simplest for now.
            schedule = schedule[1:]
        elif schedule[0][-1] != 0:
            print("Error: first schedule entry is invalid.")
            sys.exit(EXIT_FAILURE)
        with open(os.path.join(logsdir, options['schedulefile']), "wb") as f:
            f.write(schedule_to_text(schedule))
        tumble_log.info("TUMBLE RESTARTING")
    else:
        #Create a new schedule from scratch
        schedule = get_tumble_schedule(options, destaddrs,
                                       wallet.get_balance_by_mixdepth())
        tumble_log.info("TUMBLE STARTING")
        with open(os.path.join(logsdir, options['schedulefile']), "wb") as f:
            f.write(schedule_to_text(schedule))
        print("Schedule written to logs/" + options['schedulefile'])
    tumble_log.info("With this schedule: ")
    tumble_log.info(pprint.pformat(schedule))

    # 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 an expected tx fee for the whole tumbling run.
    # This is very rough: we guess with 2 inputs and 2 outputs each.
    fee_per_cp_guess = estimate_tx_fee(2,
                                       2,
                                       txtype=wallet_service.get_txtype())
    log.debug("Estimated miner/tx fee for each cj participant: " +
              str(fee_per_cp_guess))

    # From the estimated tx fees, check if the expected amount is a
    # significant value compared the the cj amount
    involved_parties = len(schedule)  # own participation in each CJ
    for item in schedule:
        involved_parties += item[2]  #  number of total tumble counterparties
    total_tumble_amount = int(0)
    max_mix_to_tumble = min(options['mixdepthsrc']+options['mixdepthcount'], \
                            max_mix_depth)
    for i in range(options['mixdepthsrc'], max_mix_to_tumble):
        total_tumble_amount += wallet_service.get_balance_by_mixdepth()[i]
        if total_tumble_amount == 0:
            raise ValueError(
                "No confirmed coins in the selected mixdepth(s). Quitting")
    exp_tx_fees_ratio = (involved_parties * fee_per_cp_guess) \
        / total_tumble_amount
    if exp_tx_fees_ratio > 0.05:
        jmprint(
            'WARNING: Expected bitcoin network miner fees for the whole '
            'tumbling run are roughly {:.1%}'.format(exp_tx_fees_ratio),
            "warning")
        if not options['restart'] and 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 for the "
                 "whole tumbling run: {:.1%}".format(exp_tx_fees_ratio))

    print("Progress logging to logs/TUMBLE.log")

    def filter_orders_callback(orders_fees, cjamount):
        """Decide whether to accept fees
        """
        return tumbler_filter_orders_callback(orders_fees, cjamount, taker)

    def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None):
        """on_finished_callback for tumbler; processing is almost entirely
        deferred to generic taker_finished in tumbler_support module, except
        here reactor signalling.
        """
        sfile = os.path.join(logsdir, options['schedulefile'])
        tumbler_taker_finished_update(taker, sfile, tumble_log, options, res,
                                      fromtx, waittime, txdetails)
        if not fromtx:
            reactor.stop()
        elif fromtx != "unconfirmed":
            reactor.callLater(waittime * 60,
                              clientfactory.getClient().clientStart)

    #instantiate Taker with given schedule and run
    taker = Taker(wallet_service,
                  schedule,
                  maxcjfee,
                  order_chooser=options['order_choose_fn'],
                  callbacks=(filter_orders_callback, None, taker_finished),
                  tdestaddrs=destaddrs)
    clientfactory = JMClientProtocolFactory(taker)
    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 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)
Exemple #18
0
def receive_snicker_main():
    usage = """ Use this script to receive proposals for SNICKER
coinjoins, parse them and then broadcast coinjoins
that fit your criteria. See the SNICKER section of
joinmarket.cfg to set your criteria.
The only argument to this script is the (JM) wallet
file against which to check.
Once all proposals have been parsed, the script will
quit.
Usage: %prog [options] wallet file [proposal]
"""
    parser = OptionParser(usage=usage)
    add_base_options(parser)
    parser.add_option('-g', '--gap-limit', action='store', type="int",
                      dest='gaplimit', default=6,
                      help='gap limit for wallet, default=6')
    parser.add_option('-m', '--mixdepth', action='store', type='int',
                      dest='mixdepth', default=0,
                      help="mixdepth to source coins from")
    parser.add_option('-a',
                      '--amtmixdepths',
                      action='store',
                      type='int',
                      dest='amtmixdepths',
                      help='number of mixdepths in wallet, default 5',
                      default=5)
    parser.add_option(
        '-n',
        '--no-upload',
        action='store_true',
        dest='no_upload',
        default=False,
        help="if set, we read the proposal from the command line"
    )

    (options, args) = parser.parse_args()
    if len(args) < 1:
        parser.error('Needs a wallet file as argument')
        sys.exit(EXIT_ARGERROR)
    wallet_name = args[0]
    snicker_plugin = JMPluginService("SNICKER")
    load_program_config(config_path=options.datadir,
                        plugin_services=[snicker_plugin])

    check_and_start_tor()

    check_regtest()

    wallet_path = get_wallet_path(wallet_name, None)
    max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1])
    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)
    snicker_plugin.start_plugin_logging(wallet_service)
    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=not options.recoversync)
    wallet_service.startService()

    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 else False
    snicker_r = SNICKERReceiver(wallet_service)
    if options.no_upload:
        proposal = args[1]
        snicker_r.process_proposals([proposal])
        return
    servers = jm_single().config.get("SNICKER", "servers").split(",")
    snicker_pf = SNICKERClientProtocolFactory(snicker_r, servers, oneshot=True)
    start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                  jm_single().config.getint("DAEMON", "daemon_port"),
                  None, snickerfactory=snicker_pf,
                  daemon=daemon)
Exemple #19
0
def main():
    parser = OptionParser(
        usage=
        'usage: %prog [options] walletname hex-tx input-index output-index net-transfer',
        description=description)
    add_base_options(parser)
    parser.add_option('-m',
                      '--mixdepth',
                      action='store',
                      type='int',
                      dest='mixdepth',
                      help='mixdepth/account to spend from, default=0',
                      default=0)
    parser.add_option('-g',
                      '--gap-limit',
                      action='store',
                      type='int',
                      dest='gaplimit',
                      default=6,
                      help='gap limit for Joinmarket wallet, default 6.')
    parser.add_option(
        '-n',
        '--no-upload',
        action='store_true',
        dest='no_upload',
        default=False,
        help="if set, we don't upload the new proposal to the servers")
    parser.add_option(
        '-f',
        '--txfee',
        action='store',
        type='int',
        dest='txfee',
        default=-1,
        help='Bitcoin miner tx_fee to use for transaction(s). A number higher '
        'than 1000 is used as "satoshi per KB" tx fee. A number lower than that '
        'uses the dynamic fee estimation of your blockchain provider as '
        'confirmation target. This temporarily overrides the "tx_fees" setting '
        'in your joinmarket.cfg. Works the same way as described in it. Check '
        'it for examples.')
    parser.add_option('-a',
                      '--amtmixdepths',
                      action='store',
                      type='int',
                      dest='amtmixdepths',
                      help='number of mixdepths in wallet, default 5',
                      default=5)
    (options, args) = parser.parse_args()
    snicker_plugin = JMPluginService("SNICKER")
    load_program_config(config_path=options.datadir,
                        plugin_services=[snicker_plugin])
    if len(args) != 5:
        jmprint("Invalid arguments, see --help")
        sys.exit(EXIT_ARGERROR)
    wallet_name, hextx, input_index, output_index, net_transfer = args
    input_index, output_index, net_transfer = [
        int(x) for x in [input_index, output_index, net_transfer]
    ]
    check_regtest()

    # 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))
    max_mix_depth = max([options.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)
    if wallet_service.rpc_error:
        sys.exit(EXIT_FAILURE)
    snicker_plugin.start_plugin_logging(wallet_service)
    # 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()

    # now that the wallet is available, we can construct a proposal
    # before encrypting it:
    originating_tx = btc.CMutableTransaction.deserialize(hextobin(hextx))
    txid1 = originating_tx.GetTxid()[::-1]
    # the proposer wallet needs to choose a single utxo, from his selected
    # mixdepth, that is bigger than the output amount of tx1 at the given
    # index.
    fee_est = estimate_tx_fee(2, 3, txtype=wallet_service.get_txtype())
    amt_required = originating_tx.vout[output_index].nValue + fee_est

    prop_utxo_dict = wallet_service.select_utxos(options.mixdepth,
                                                 amt_required)
    prop_utxos = list(prop_utxo_dict)
    prop_utxo_vals = [prop_utxo_dict[x] for x in prop_utxos]
    # get the private key for that utxo
    priv = wallet_service.get_key_from_addr(
        wallet_service.script_to_addr(prop_utxo_vals[0]['script']))
    # construct the arguments for the snicker proposal:
    our_input_utxos = [
        btc.CMutableTxOut(x['value'], x['script']) for x in prop_utxo_vals
    ]

    # destination must be a different mixdepth:
    prop_destn_spk = wallet_service.get_new_script(
        (options.mixdepth + 1) % (wallet_service.mixdepth + 1), 1)
    change_spk = wallet_service.get_new_script(options.mixdepth, 1)
    their_input = (txid1, output_index)
    # we also need to extract the pubkey of the chosen input from
    # the witness; we vary this depending on our wallet type:
    pubkey, msg = btc.extract_pubkey_from_witness(originating_tx, input_index)
    if not pubkey:
        log.error("Failed to extract pubkey from transaction: {}".format(msg))
        sys.exit(EXIT_FAILURE)
    encrypted_proposal = wallet_service.create_snicker_proposal(
        prop_utxos,
        their_input,
        our_input_utxos,
        originating_tx.vout[output_index],
        net_transfer,
        fee_est,
        priv,
        pubkey,
        prop_destn_spk,
        change_spk,
        version_byte=1) + b"," + bintohex(pubkey).encode('utf-8')
    if options.no_upload:
        jmprint(encrypted_proposal.decode("utf-8"))
        sys.exit(EXIT_SUCCESS)

    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 else False
    snicker_client = SNICKERPostingClient([encrypted_proposal])
    servers = jm_single().config.get("SNICKER", "servers").split(",")
    snicker_pf = SNICKERClientProtocolFactory(snicker_client, servers)
    start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                  jm_single().config.getint("DAEMON", "daemon_port"),
                  None,
                  snickerfactory=snicker_pf,
                  daemon=daemon)
def receive_payjoin_main():
    parser = OptionParser(
        usage='usage: %prog [options] [wallet file] [amount-to-receive]')
    add_base_options(parser)
    parser.add_option(
        '-P',
        '--hs-port',
        action='store',
        type='int',
        dest='hsport',
        default=80,
        help='port on which to serve the ephemeral hidden service.')
    parser.add_option('-g',
                      '--gap-limit',
                      action='store',
                      type="int",
                      dest='gaplimit',
                      default=6,
                      help='gap limit for wallet, default=6')
    parser.add_option('-m',
                      '--mixdepth',
                      action='store',
                      type='int',
                      dest='mixdepth',
                      default=0,
                      help="mixdepth to source coins from")
    parser.add_option('-a',
                      '--amtmixdepths',
                      action='store',
                      type='int',
                      dest='amtmixdepths',
                      help='number of mixdepths in wallet, default 5',
                      default=5)

    (options, args) = parser.parse_args()
    if len(args) < 2:
        parser.error(
            'Needs a wallet, and a receiving amount in bitcoins or satoshis')
        sys.exit(EXIT_ARGERROR)
    wallet_name = args[0]
    try:
        # amount is stored internally in sats, but will be decimal in URL.
        bip78_amount = amount_to_sat(args[1])
    except:
        parser.error("Invalid receiving amount passed: " + bip78_amount)
        sys.exit(EXIT_FAILURE)
    if bip78_amount < 0:
        parser.error("Receiving amount must be a positive number")
        sys.exit(EXIT_FAILURE)
    load_program_config(config_path=options.datadir)

    check_and_start_tor()

    check_regtest()

    wallet_path = get_wallet_path(wallet_name, None)
    max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1])
    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)

    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=not options.recoversync)
    wallet_service.startService()
    # having enforced wallet sync, we can check if we have coins
    # to do payjoin in the mixdepth
    if wallet_service.get_balance_by_mixdepth(
            minconfs=1)[options.mixdepth] == 0:
        jlog.error("Cannot do payjoin from mixdepth " + str(options.mixdepth) +
                   ", no confirmed coins. Shutting down.")
        sys.exit(EXIT_ARGERROR)
    receiver_manager = JMBIP78ReceiverManager(wallet_service, options.mixdepth,
                                              bip78_amount, options.hsport)
    reactor.callWhenRunning(receiver_manager.initiate)
    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")
    # JM is default, so must be switched off explicitly in this call:
    start_reactor(dhost, dport, bip78=True, jm_coinjoin=False, daemon=daemon)
Exemple #21
0
def ygmain(ygclass, txfee=1000, cjfee_a=200, cjfee_r=0.002, ordertype='swreloffer',
           nickserv_password='', minsize=100000, gaplimit=6):
    import sys

    parser = OptionParser(usage='usage: %prog [options] [wallet file]')
    parser.add_option('-o', '--ordertype', action='store', type='string',
                      dest='ordertype', default=ordertype,
                      help='type of order; can be either reloffer or absoffer')
    parser.add_option('-t', '--txfee', action='store', type='int',
                      dest='txfee', default=txfee,
                      help='minimum miner fee in satoshis')
    parser.add_option('-c', '--cjfee', action='store', type='string',
                      dest='cjfee', default='',
                      help='requested coinjoin fee in satoshis or proportion')
    parser.add_option('-p', '--password', action='store', type='string',
                      dest='password', default=nickserv_password,
                      help='irc nickserv password')
    parser.add_option('-s', '--minsize', action='store', type='int',
                      dest='minsize', default=minsize,
                      help='minimum coinjoin size in satoshis')
    parser.add_option('-g', '--gap-limit', action='store', type="int",
                      dest='gaplimit', default=gaplimit,
                      help='gap limit for wallet, default='+str(gaplimit))
    parser.add_option('--fast',
                      action='store_true',
                      dest='fastsync',
                      default=False,
                      help=('choose to do fast wallet sync, only for Core and '
                            'only for previously synced wallet'))
    parser.add_option('-m', '--mixdepth', action='store', type='int',
                      dest='mixdepth', default=None,
                      help="highest mixdepth to use")
    (options, args) = parser.parse_args()
    if len(args) < 1:
        parser.error('Needs a wallet')
        sys.exit(0)
    wallet_name = args[0]
    ordertype = options.ordertype
    txfee = options.txfee
    if ordertype in ('reloffer', 'swreloffer'):
        if options.cjfee != '':
            cjfee_r = options.cjfee
        # minimum size is such that you always net profit at least 20%
        #of the miner fee
        minsize = max(int(1.2 * txfee / float(cjfee_r)), options.minsize)
    elif ordertype in ('absoffer', 'swabsoffer'):
        if options.cjfee != '':
            cjfee_a = int(options.cjfee)
        minsize = options.minsize
    else:
        parser.error('You specified an incorrect offer type which ' +\
                     'can be either swreloffer or swabsoffer')
        sys.exit(0)
    nickserv_password = options.password

    load_program_config()

    wallet_path = get_wallet_path(wallet_name, 'wallets')
    wallet = open_test_wallet_maybe(
        wallet_path, wallet_name, options.mixdepth,
        gap_limit=options.gaplimit)

    if jm_single().config.get("BLOCKCHAIN", "blockchain_source") == "electrum-server":
        jm_single().bc_interface.synctype = "with-script"

    while not jm_single().bc_interface.wallet_synced:
        sync_wallet(wallet, fast=options.fastsync)

    maker = ygclass(wallet, [options.txfee, cjfee_a, cjfee_r,
                             options.ordertype, options.minsize])
    jlog.info('starting yield generator')
    clientfactory = JMClientProtocolFactory(maker, proto_type="MAKER")

    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 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)
def main():
    parser = get_sendpayment_parser()
    (options, args) = parser.parse_args()
    load_program_config()
    if options.schedule == '' and len(args) < 3:
        parser.error('Needs a 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:
            print('ERROR: Address invalid. ' + errormsg)
            return
        schedule = [[options.mixdepth, amount, options.makercount,
                     destaddr, 0.0, 0]]
    else:
        result, schedule = get_schedule(options.schedule)
        if not result:
            log.info("Failed to load schedule file, quitting. Check the syntax.")
            log.info("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]

    #to allow testing of confirm/unconfirm callback for multiple txs
    if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface):
        jm_single().bc_interface.tick_forward_chain_interval = 10
        jm_single().bc_interface.simulating = True
        jm_single().maker_timeout_sec = 15

    chooseOrdersFunc = None
    if options.pickorders:
        chooseOrdersFunc = pick_order
        if sweeping:
            print('WARNING: You may have to pick offers multiple times')
            print('WARNING: due to manual offer picking while sweeping')
    elif options.choosecheapest:
        chooseOrdersFunc = cheapest_order_choose
    else:  # choose randomly (weighted)
        chooseOrdersFunc = weighted_order_choose

    # 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)

    log.debug('starting sendpayment')

    if not options.userpcwallet:
        #maxmixdepth in the wallet is actually the *number* of mixdepths (so misnamed);
        #to ensure we have enough, must be at least (requested index+1)
        max_mix_depth = max([mixdepth+1, options.amtmixdepths])

        wallet_path = get_wallet_path(wallet_name, None)
        wallet = open_test_wallet_maybe(
            wallet_path, wallet_name, max_mix_depth, gap_limit=options.gaplimit)
    else:
        raise NotImplemented("Using non-joinmarket wallet is not supported.")
    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)
    if options.makercount == 0:
        direct_send(wallet, amount, mixdepth, destaddr, options.answeryes)
        return

    if wallet.get_txtype() == 'p2pkh':
        print("Only direct sends (use -N 0) are supported for "
              "legacy (non-segwit) wallets.")
        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 raw_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
                print("We failed to complete the transaction. The following "
                      "makers responded honestly: ", taker.honest_makers,
                      ", so we will retry with them.")
                #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()

    taker = Taker(wallet,
                  schedule,
                  order_chooser=chooseOrdersFunc,
                  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
    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)
def main():
    tumble_log = get_tumble_log(logsdir)
    (options, args) = get_tumbler_parser().parse_args()
    options_org = options
    options = vars(options)
    if len(args) < 1:
        print('Error: Needs a wallet file')
        sys.exit(0)
    load_program_config()

    #Load the wallet
    wallet_name = args[0]
    max_mix_depth = options['mixdepthsrc'] + options['mixdepthcount']
    wallet_path = get_wallet_path(wallet_name, None)
    wallet = open_test_wallet_maybe(wallet_path, wallet_name, max_mix_depth)
    if jm_single().config.get("BLOCKCHAIN",
                              "blockchain_source") == "electrum-server":
        jm_single().bc_interface.synctype = "with-script"
    while not jm_single().bc_interface.wallet_synced:
        sync_wallet(wallet, fast=options['fastsync'])

    maxcjfee = get_max_cj_fee_values(jm_single().config, options_org)
    log.info(
        "Using maximum coinjoin fee limits per maker of {:.4%}, {} sat".format(
            *maxcjfee))

    #Parse options and generate schedule
    #Output information to log files
    jm_single().mincjamount = options['mincjamount']
    destaddrs = args[1:]
    print(destaddrs)
    #If the --restart flag is set we read the schedule
    #from the file, and filter out entries that are
    #already complete
    if options['restart']:
        res, schedule = get_schedule(
            os.path.join(logsdir, options['schedulefile']))
        if not res:
            print("Failed to load schedule, name: " +
                  str(options['schedulefile']))
            print("Error was: " + str(schedule))
            sys.exit(0)
        #This removes all entries that are marked as done
        schedule = [s for s in schedule if s[5] != 1]
        if isinstance(schedule[0][5], str) and len(schedule[0][5]) == 64:
            #ensure last transaction is confirmed before restart
            tumble_log.info("WAITING TO RESTART...")
            txid = schedule[0][5]
            restart_waiter(txid + ":0")  #add 0 index because all have it
            #remove the already-done entry (this connects to the other TODO,
            #probably better *not* to truncate the done-already txs from file,
            #but simplest for now.
            schedule = schedule[1:]
        elif schedule[0][5] != 0:
            print("Error: first schedule entry is invalid.")
            sys.exit(0)
        with open(os.path.join(logsdir, options['schedulefile']), "wb") as f:
            f.write(schedule_to_text(schedule))
        tumble_log.info("TUMBLE RESTARTING")
    else:
        #Create a new schedule from scratch
        schedule = get_tumble_schedule(options, destaddrs)
        tumble_log.info("TUMBLE STARTING")
        with open(os.path.join(logsdir, options['schedulefile']), "wb") as f:
            f.write(schedule_to_text(schedule))
        print("Schedule written to logs/" + options['schedulefile'])
    tumble_log.info("With this schedule: ")
    tumble_log.info(pprint.pformat(schedule))

    print("Progress logging to logs/TUMBLE.log")

    def filter_orders_callback(orders_fees, cjamount):
        """Decide whether to accept fees
        """
        return tumbler_filter_orders_callback(orders_fees, cjamount, taker,
                                              options)

    def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None):
        """on_finished_callback for tumbler; processing is almost entirely
        deferred to generic taker_finished in tumbler_support module, except
        here reactor signalling.
        """
        sfile = os.path.join(logsdir, options['schedulefile'])
        tumbler_taker_finished_update(taker, sfile, tumble_log, options, res,
                                      fromtx, waittime, txdetails)
        if not fromtx:
            reactor.stop()
        elif fromtx != "unconfirmed":
            reactor.callLater(waittime * 60,
                              clientfactory.getClient().clientStart)

    #to allow testing of confirm/unconfirm callback for multiple txs
    if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface):
        jm_single().bc_interface.tick_forward_chain_interval = 10
        jm_single().bc_interface.simulating = True
        jm_single().maker_timeout_sec = 15

    #instantiate Taker with given schedule and run
    taker = Taker(wallet,
                  schedule,
                  order_chooser=options['order_choose_fn'],
                  max_cj_fee=maxcjfee,
                  callbacks=(filter_orders_callback, None, taker_finished),
                  tdestaddrs=destaddrs)
    clientfactory = JMClientProtocolFactory(taker)
    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 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)
def test_start_yg_and_taker_setup(setup_onion_ygrunner):
    """Set up some wallets, for the ygs and 1 taker.
    Then start LN and the ygs in the background, then fire
    a startup of a wallet daemon for the taker who then
    makes a coinjoin payment.
    """
    if jm_single().config.get("POLICY", "native") == "true":
        walletclass = SegwitWallet
    else:
        # TODO add Legacy
        walletclass = SegwitLegacyWallet

    start_bot_num, end_bot_num = [int(x) for x in jm_single().config.get(
        "MESSAGING:onion", "regtest_count").split(",")]
    num_ygs = end_bot_num - start_bot_num
    # specify the number of wallets and bots of each type:
    wallet_services = make_wallets(num_ygs + 1,
                           wallet_structures=[[1, 3, 0, 0, 0]] * (num_ygs + 1),
                           mean_amt=2.0,
                           walletclass=walletclass)
    #the sendpayment bot uses the last wallet in the list
    wallet_service = wallet_services[end_bot_num - 1]['wallet']
    jmprint("\n\nTaker wallet seed : " + wallet_services[end_bot_num - 1]['seed'])
    # for manual audit if necessary, show the maker's wallet seeds
    # also (note this audit should be automated in future)
    jmprint("\n\nMaker wallet seeds: ")
    for i in range(start_bot_num, end_bot_num):
        jmprint("Maker seed: " + wallet_services[i - 1]['seed'])
    jmprint("\n")
    wallet_service.sync_wallet(fast=True)
    ygclass = YieldGeneratorBasic

    # As per previous note, override non-default command line settings:
    options = {}
    for x in ["ordertype", "txfee_contribution", "txfee_contribution_factor",
              "cjfee_a", "cjfee_r", "cjfee_factor", "minsize", "size_factor"]:
        options[x] = jm_single().config.get("YIELDGENERATOR", x)
    ordertype = options["ordertype"]
    txfee_contribution = int(options["txfee_contribution"])
    txfee_contribution_factor = float(options["txfee_contribution_factor"])
    cjfee_factor = float(options["cjfee_factor"])
    size_factor = float(options["size_factor"])
    if ordertype == 'reloffer':
        cjfee_r = options["cjfee_r"]
        # minimum size is such that you always net profit at least 20%
        #of the miner fee
        minsize = max(int(1.2 * txfee_contribution / float(cjfee_r)),
            int(options["minsize"]))
        cjfee_a = None
    elif ordertype == 'absoffer':
        cjfee_a = int(options["cjfee_a"])
        minsize = int(options["minsize"])
        cjfee_r = None
    else:
        assert False, "incorrect offertype config for yieldgenerator."

    txtype = wallet_service.get_txtype()
    if txtype == "p2wpkh":
        prefix = "sw0"
    elif txtype == "p2sh-p2wpkh":
        prefix = "sw"
    elif txtype == "p2pkh":
        prefix = ""
    else:
        assert False, "Unsupported wallet type for yieldgenerator: " + txtype

    ordertype = prefix + ordertype

    for i in range(start_bot_num, end_bot_num):
        cfg = [txfee_contribution, cjfee_a, cjfee_r, ordertype, minsize,
               txfee_contribution_factor, cjfee_factor, size_factor]
        wallet_service_yg = wallet_services[i - 1]["wallet"]

        wallet_service_yg.startService()

        yg = ygclass(wallet_service_yg, cfg)
        clientfactory = RegtestJMClientProtocolFactory(yg, proto_type="MAKER")
        # This ensures that the right rpc/port config is passed into the daemon,
        # for this specific bot:
        clientfactory.i = i
        # This ensures that this bot knows which other bots are directory nodes:
        clientfactory.set_directory_nodes(directory_node_indices)
        nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
        daemon = bool(nodaemon)
        #rs = True if i == num_ygs - 1 else False
        start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                      jm_single().config.getint("DAEMON", "daemon_port"),
                      clientfactory, daemon=daemon, rs=False)
    reactor.callLater(1.0, start_test_taker, wallet_services[end_bot_num - 1]['wallet'], end_bot_num, num_ygs)
    reactor.run()