def cli_get_wallet_passphrase_check():
    password = get_password('Enter wallet file encryption passphrase: ')
    password2 = get_password('Reenter wallet file encryption passphrase: ')
    if password != password2:
        jmprint('ERROR. Passwords did not match', "error")
        return False
    return password
 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()
Esempio n. 3
0
    def import_addresses(self, addr_list, wallet_name, restart_cb=None):
        """Imports addresses in a batch during initial sync.
        Refuses to proceed if keys are found to be under control
        of another account/label (see console output), and quits.
        Do NOT use for in-run imports, use rpc('importaddress',..) instead.
        """
        requests = []
        for addr in addr_list:
            requests.append({
                "scriptPubKey": {"address": addr},
                "timestamp": 0,
                "label": wallet_name,
                "watchonly": True
            })

        result = self._rpc('importmulti', [requests, {"rescan": False}])

        num_failed = 0
        for row in result:
            if row['success'] == False:
                num_failed += 1
                # don't try/catch, assume failure always has error message
                log.warn(row['error']['message'])
        if num_failed > 0:
            fatal_msg = ("Fatal sync error: import of {} address(es) failed for "
                         "some reason. To prevent coin or privacy loss, "
                         "Joinmarket will not load a wallet in this conflicted "
                         "state. Try using a new Bitcoin Core wallet to sync this "
                         "Joinmarket wallet, or use a new Joinmarket wallet."
                         "".format(num_failed))
            if restart_cb:
                restart_cb(fatal_msg)
            else:
                jmprint(fatal_msg, "important")
            sys.exit(EXIT_FAILURE)
Esempio n. 4
0
def main():
    parser = OptionParser(
        usage='usage: %prog [options] wallet_file_name',
        description='Create a wallet with the given wallet name')
    (options, args) = parser.parse_args()

    load_program_config(config_path="/data/.joinmarket")
    with open(os.path.join(jm_single().datadir, "jm-wallet-seed"),
              "r") as file:
        words = file.read().replace('\n', '')
        words.strip()
    with open(os.path.join(jm_single().datadir, "jm-wallet-password"),
              "r") as file:
        password = file.read().replace('\n', '').encode("utf-8")
    entropy = SegwitLegacyWallet.entropy_from_mnemonic(str(words))
    wallet_root_path = os.path.join(jm_single().datadir, "wallets")
    # add wallet as first argument
    wallet_name = os.path.join(wallet_root_path, args[0])
    wallet = create_wallet(wallet_name,
                           password,
                           4,
                           SegwitLegacyWallet,
                           entropy=entropy,
                           entropy_extension=None)
    jmprint("recovery_seed:{}".format(wallet.get_mnemonic_words()[0]),
            "important")
    wallet.close()
def main():
    parser = OptionParser(
        usage='usage: %prog [options] wallet_file_name password',
        description='Create a wallet with the given wallet name and password.')
    add_base_options(parser)
    (options, args) = parser.parse_args()
    if options.wallet_password_stdin:
        stdin = sys.stdin.read()
        password = stdin.encode("utf-8")
    else:
        assert len(
            args
        ) > 1, "must provide password via stdin (see --help), or as second argument."
        password = args[1].encode("utf-8")
    load_program_config(config_path=options.datadir)
    wallet_root_path = os.path.join(jm_single().datadir, "wallets")
    wallet_name = os.path.join(wallet_root_path, args[0])
    if jm_single().config.get("POLICY", "native") == "true":
        walletclass = SegwitWallet
    else:
        walletclass = SegwitLegacyWallet
    wallet = create_wallet(wallet_name, password, 4, walletclass)
    jmprint("recovery_seed:{}".format(wallet.get_mnemonic_words()[0]),
            "important")
    wallet.close()
def cli_get_mnemonic_extension():
    uin = input("Would you like to use a two-factor mnemonic recovery "
                "phrase? write 'n' if you don't know what this is (y/n): ")
    if len(uin) == 0 or uin[0] != 'y':
        jmprint("Not using mnemonic extension", "info")
        return None  #no mnemonic extension
    jmprint("Note: This will be stored in a reversible way. Do not reuse!",
            "info")
    return input("Enter mnemonic extension: ")
def open_wallet(path,
                ask_for_password=True,
                password=None,
                read_only=False,
                **kwargs):
    """
    Open the wallet file at path and return the corresponding wallet object.

    params:
        path: str, full path to wallet file
        ask_for_password: bool, if False password is assumed unset and user
            will not be asked to type it
        password: password for storage, ignored if ask_for_password is True
        read_only: bool, if True, open wallet in read-only mode
        kwargs: additional options to pass to wallet's init method

    returns:
        wallet object
    """
    if not os.path.isfile(path):
        raise Exception(
            "Failed to open wallet at '{}': not a file".format(path))

    if not Storage.is_storage_file(path):
        raise Exception("Failed to open wallet at '{}': not a valid joinmarket"
                        " wallet.\n\nIf this wallet is in the old json format "
                        "you need to convert it using the conversion script"
                        "at `scripts/convert_old_wallet.py`".format(path))

    if ask_for_password and Storage.is_encrypted_storage_file(path):
        while True:
            try:
                # do not try empty password, assume unencrypted on empty password
                pwd = get_password(
                    "Enter wallet decryption passphrase: ") or None
                storage = Storage(path, password=pwd, read_only=read_only)
            except StoragePasswordError:
                jmprint("Wrong password, try again.", "warning")
                continue
            except Exception as e:
                jmprint("Failed to load wallet, error message: " + repr(e),
                        "error")
                raise e
            break
    else:
        storage = Storage(path, password, read_only=read_only)

    wallet_cls = get_wallet_cls_from_storage(storage)
    wallet = wallet_cls(storage, **kwargs)
    wallet_sanity_check(wallet)
    return wallet
 def print_row(index, time, tx_type, amount, delta, balance, cj_n,
               miner_fees, utxo_count, mixdepth_src, mixdepth_dst, txid):
     data = [
         index,
         datetime.fromtimestamp(time).strftime("%Y-%m-%d %H:%M"), tx_type,
         sat_to_str(amount),
         sat_to_str_p(delta),
         sat_to_str(balance),
         skip_n1(cj_n),
         sat_to_str(miner_fees),
         '% 3d' % utxo_count,
         skip_n1(mixdepth_src),
         skip_n1(mixdepth_dst)
     ]
     if options.verbosity % 2 == 0: data += [txid]
     jmprint(s().join(map('"{}"'.format, data)), "info")
Esempio n. 9
0
 def display_rescan_message_and_system_exit(self, restart_cb):
     #TODO using system exit here should be avoided as it makes the code
     # harder to understand and reason about
     #theres also a sys.exit() in BitcoinCoreInterface.import_addresses()
     #perhaps have sys.exit() placed inside the restart_cb that only
     # CLI scripts will use
     if self.bci.__class__ == BitcoinCoreInterface:
         #Exit conditions cannot be included in tests
         restart_msg = ("restart Bitcoin Core with -rescan or use "
                        "`bitcoin-cli rescanblockchain` if you're "
                        "recovering an existing wallet from backup seed\n"
                        "Otherwise just restart this joinmarket application.")
         if restart_cb:
             restart_cb(restart_msg)
         else:
             jmprint(restart_msg, "important")
             sys.exit(EXIT_SUCCESS)
 def add_watchonly_addresses(self, addr_list, wallet_name, restart_cb=None):
     """For backwards compatibility, this fn name is preserved
     as the case where we quit the program if a rescan is required;
     but in some cases a rescan is not required (if the address is known
     to be new/unused). For that case use import_addresses instead.
     """
     self.import_addresses(addr_list, wallet_name)
     if jm_single().config.get("BLOCKCHAIN",
                               "blockchain_source") != 'regtest': #pragma: no cover
         #Exit conditions cannot be included in tests
         restart_msg = ("restart Bitcoin Core with -rescan if you're "
                        "recovering an existing wallet from backup seed\n"
                        "Otherwise just restart this joinmarket application.")
         if restart_cb:
             restart_cb(restart_msg)
         else:
             jmprint(restart_msg, "important")
             sys.exit(0)
def wallet_generate_recover(method,
                            walletspath,
                            default_wallet_name='wallet.jmdat',
                            mixdepth=DEFAULT_MIXDEPTH):
    if is_segwit_mode():
        #Here using default callbacks for scripts (not used in Qt)
        return wallet_generate_recover_bip39(method,
                                             walletspath,
                                             default_wallet_name,
                                             mixdepth=mixdepth)

    entropy = None
    if method == 'recover':
        seed = input("Input 12 word recovery seed: ")
        try:
            entropy = LegacyWallet.entropy_from_mnemonic(seed)
        except WalletError as e:
            jmprint("Unable to restore seed: {}".format(e.message), "error")
            return ""
    elif method != 'generate':
        raise Exception(
            "unknown method for wallet creation: '{}'".format(method))

    password = cli_get_wallet_passphrase_check()
    if not password:
        return ""

    wallet_name = cli_get_wallet_file_name()
    if not wallet_name:
        wallet_name = default_wallet_name
    wallet_path = os.path.join(walletspath, wallet_name)

    wallet = create_wallet(wallet_path,
                           password,
                           mixdepth,
                           wallet_cls=LegacyWallet,
                           entropy=entropy)
    jmprint(
        "Write down and safely store this wallet recovery seed\n\n{}\n".format(
            wallet.get_mnemonic_words()[0]), "important")
    wallet.close()
    return True
Esempio n. 12
0
def main():
    parser = OptionParser(
    usage='usage: %prog [options] wallet_file_name password',
    description='Create a wallet with the given wallet name and password.')
    (options, args) = parser.parse_args()

    # Load up defaults
    load_program_config(config_path='/data/.joinmarket')
    wallet_root_path = os.path.join(jm_single().datadir, "wallets")
    # get wallet from first argument
    wallet_name = os.path.join(wallet_root_path, args[0])
    wallet = create_wallet(wallet_name, args[1].encode("utf-8"), 4, SegwitLegacyWallet)
    # Open file for writing
    seedfile = open(os.path.join(jm_single().datadir, "jm-wallet-seed"), "w")
    seedfile.write(wallet.get_mnemonic_words()[0])
    seedfile.write("\n")
    seedfile.close()

    jmprint("recovery_seed:{}"
         .format(wallet.get_mnemonic_words()[0]), "important")
    wallet.close()
def main():
    parser = OptionParser(
        usage='usage: %prog [options] wallet_file_name [password]',
        description='Create a wallet with the given wallet name and password.')
    add_base_options(parser)
    parser.add_option(
        '--recovery-seed-file',
        dest='seed_file',
        default=None,
        help=
        ('File containing a mnemonic recovery phrase. If provided, the wallet '
         'is recovered from this seed instead of being newly generated.'))
    (options, args) = parser.parse_args()
    wallet_name = args[0]
    if options.wallet_password_stdin:
        password = wallet_utils.read_password_stdin()
    else:
        assert len(
            args
        ) > 1, "must provide password via stdin (see --help), or as second argument."
        password = args[1].encode("utf-8")
    seed = options.seed_file and Path(options.seed_file).read_text().rstrip()

    load_program_config(config_path=options.datadir)
    wallet_root_path = os.path.join(jm_single().datadir, "wallets")
    wallet_path = os.path.join(wallet_root_path, wallet_name)
    if jm_single().config.get("POLICY", "native") == "true":
        walletclass = SegwitWalletFidelityBonds
    else:
        # Fidelity Bonds are not available for segwit legacy wallets
        walletclass = SegwitLegacyWallet
    entropy = seed and SegwitLegacyWallet.entropy_from_mnemonic(seed)
    wallet = create_wallet(wallet_path,
                           password,
                           wallet_utils.DEFAULT_MIXDEPTH,
                           walletclass,
                           entropy=entropy)
    jmprint("recovery_seed:{}".format(wallet.get_mnemonic_words()[0]),
            "important")
    wallet.close()
def wallet_importprivkey(wallet, mixdepth, key_type):
    jmprint(
        "WARNING: This imported key will not be recoverable with your 12 "
        "word mnemonic phrase. Make sure you have backups.", "warning")
    jmprint(
        "WARNING: Handling of raw ECDSA bitcoin private keys can lead to "
        "non-intuitive behaviour and loss of funds.\n  Recommended instead "
        "is to use the \'sweep\' feature of sendpayment.py.", "warning")
    privkeys = input("Enter private key(s) to import: ")
    privkeys = privkeys.split(',') if ',' in privkeys else privkeys.split()
    imported_addr = []
    import_failed = 0
    # TODO read also one key for each line
    for wif in privkeys:
        # TODO is there any point in only accepting wif format? check what
        # other wallets do
        try:
            path = wallet.import_private_key(mixdepth, wif, key_type=key_type)
        except WalletError as e:
            print("Failed to import key {}: {}".format(wif, e))
            import_failed += 1
        else:
            imported_addr.append(wallet.get_addr_path(path))

    if not imported_addr:
        jmprint("Warning: No keys imported!", "error")
        return

    wallet.save()

    # show addresses to user so they can verify everything went as expected
    jmprint(
        "Imported keys for addresses:\n{}".format('\n'.join(imported_addr)),
        "success")
    if import_failed:
        jmprint("Warning: failed to import {} keys".format(import_failed),
                "error")
def pick_order(orders, n): #pragma: no cover
    jmprint("Considered orders:", "info")
    for i, o in enumerate(orders):
        jmprint("    %2d. %20s, CJ fee: %6s, tx fee: %6d" %
              (i, o[0]['counterparty'], str(o[0]['cjfee']), o[0]['txfee']), "info")
    pickedOrderIndex = -1
    if i == 0:
        jmprint("Only one possible pick, picking it.", "info")
        return orders[0]
    while pickedOrderIndex == -1:
        try:
            pickedOrderIndex = int(input('Pick an order between 0 and ' +
                                             str(i) + ': '))
        except ValueError:
            pickedOrderIndex = -1
            continue

        if 0 <= pickedOrderIndex < len(orders):
            return orders[pickedOrderIndex]
        pickedOrderIndex = -1
Esempio n. 16
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)
Esempio n. 17
0
            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)


if __name__ == "__main__":
    main()
    jmprint('done', "success")
Esempio n. 18
0
def load_program_config(config_path=None, bs=None):
    global_singleton.config.readfp(io.StringIO(defaultconfig))
    remove_unwanted_default_settings(global_singleton.config)
    if not config_path:
        config_path = os.getcwd()
    global_singleton.config_location = os.path.join(
        config_path, global_singleton.config_location)
    loadedFiles = global_singleton.config.read(
        [global_singleton.config_location])
    #Hack required for electrum; must be able to enforce a different
    #blockchain interface even in default/new load.
    if bs:
        global_singleton.config.set("BLOCKCHAIN", "blockchain_source", bs)
    # Create default config file if not found
    if len(loadedFiles) != 1:
        with open(global_singleton.config_location, "w") as configfile:
            configfile.write(defaultconfig)
        jmprint(
            "Created a new `joinmarket.cfg`. Please review and adopt the "
            "settings and restart joinmarket.", "info")
        exit(1)

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

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

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

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

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

    #set the location of the commitments file
    try:
        global_singleton.commit_file_location = global_singleton.config.get(
            "POLICY", "commit_file_location")
    except NoOptionError:  #pragma: no cover
        log.debug("No commitment file location in config, using default "
                  "location cmtdata/commitments.json")
    set_commitment_file(
        os.path.join(config_path, global_singleton.commit_file_location))
def wallet_dumpprivkey(wallet, hdpath):
    if not hdpath:
        jmprint("Error: no hd wallet path supplied", "error")
        return ""
    path = wallet.path_repr_to_path(hdpath)
    return wallet.get_wif_path(path)  # will raise exception on invalid path
Esempio n. 20
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)
def wallet_tool_main(wallet_root_path):
    """Main wallet tool script function; returned is a string (output or error)
    """
    parser = get_wallettool_parser()
    (options, args) = parser.parse_args()

    noseed_methods = ['generate', 'recover']
    methods = [
        'display', 'displayall', 'summary', 'showseed', 'importprivkey',
        'history', 'showutxos'
    ]
    methods.extend(noseed_methods)
    noscan_methods = [
        'showseed', 'importprivkey', 'dumpprivkey', 'signmessage'
    ]
    readonly_methods = [
        'display', 'displayall', 'summary', 'showseed', 'history', 'showutxos',
        'dumpprivkey', 'signmessage'
    ]

    if len(args) < 1:
        parser.error('Needs a wallet file or method')
        sys.exit(0)

    if options.mixdepth is not None and options.mixdepth < 0:
        parser.error("Must have at least one mixdepth.")
        sys.exit(0)

    if args[0] in noseed_methods:
        method = args[0]
        if options.mixdepth is None:
            options.mixdepth = DEFAULT_MIXDEPTH
    else:
        seed = args[0]
        wallet_path = get_wallet_path(seed, wallet_root_path)
        method = ('display' if len(args) == 1 else args[1].lower())
        read_only = method in readonly_methods

        wallet = open_test_wallet_maybe(wallet_path,
                                        seed,
                                        options.mixdepth,
                                        read_only=read_only,
                                        gap_limit=options.gaplimit)

        if method not in noscan_methods:
            # if nothing was configured, we override bitcoind's options so that
            # unconfirmed balance is included in the wallet display by default
            if 'listunspent_args' not in jm_single().config.options('POLICY'):
                jm_single().config.set('POLICY', 'listunspent_args', '[0]')
            while not jm_single().bc_interface.wallet_synced:
                sync_wallet(wallet, fast=options.fastsync)
    #Now the wallet/data is prepared, execute the script according to the method
    if method == "display":
        return wallet_display(wallet, options.gaplimit, options.showprivkey)
    elif method == "displayall":
        return wallet_display(wallet,
                              options.gaplimit,
                              options.showprivkey,
                              displayall=True)
    elif method == "summary":
        return wallet_display(wallet,
                              options.gaplimit,
                              options.showprivkey,
                              summarized=True)
    elif method == "history":
        if not isinstance(jm_single().bc_interface, BitcoinCoreInterface):
            jmprint(
                'showing history only available when using the Bitcoin Core ' +
                'blockchain interface', "error")
            sys.exit(0)
        else:
            return wallet_fetch_history(wallet, options)
    elif method == "generate":
        retval = wallet_generate_recover("generate",
                                         wallet_root_path,
                                         mixdepth=options.mixdepth)
        return "Generated wallet OK" if retval else "Failed"
    elif method == "recover":
        retval = wallet_generate_recover("recover",
                                         wallet_root_path,
                                         mixdepth=options.mixdepth)
        return "Recovered wallet OK" if retval else "Failed"
    elif method == "showutxos":
        return wallet_showutxos(wallet, options.showprivkey)
    elif method == "showseed":
        return wallet_showseed(wallet)
    elif method == "dumpprivkey":
        return wallet_dumpprivkey(wallet, options.hd_path)
    elif method == "importprivkey":
        #note: must be interactive (security)
        if options.mixdepth is None:
            parser.error("You need to specify a mixdepth with -m")
        wallet_importprivkey(wallet, options.mixdepth,
                             map_key_type(options.key_type))
        return "Key import completed."
    elif method == "signmessage":
        return wallet_signmessage(wallet, options.hd_path, args[2])
Esempio n. 22
0
def load_program_config(config_path="", bs=None, plugin_services=[]):
    global_singleton.config.readfp(io.StringIO(defaultconfig))
    if not config_path:
        config_path = lookup_appdata_folder(global_singleton.APPNAME)
    # we set the global home directory, but keep the config_path variable
    # for callers of this function:
    global_singleton.datadir = config_path
    jmprint("User data location: " + global_singleton.datadir, "info")
    if not os.path.exists(global_singleton.datadir):
        os.makedirs(global_singleton.datadir)
    # prepare folders for wallets and logs
    if not os.path.exists(os.path.join(global_singleton.datadir, "wallets")):
        os.makedirs(os.path.join(global_singleton.datadir, "wallets"))
    if not os.path.exists(os.path.join(global_singleton.datadir, "logs")):
        os.makedirs(os.path.join(global_singleton.datadir, "logs"))
    if not os.path.exists(os.path.join(global_singleton.datadir, "cmtdata")):
        os.makedirs(os.path.join(global_singleton.datadir, "cmtdata"))
    global_singleton.config_location = os.path.join(
        global_singleton.datadir, global_singleton.config_location)

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

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

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

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

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

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

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

    for p in plugin_services:
        # for now, at this config level, the only significance
        # of a "plugin" is that it keeps its own separate log.
        # We require that a section exists in the config file,
        # and that it has enabled=true:
        assert isinstance(p, JMPluginService)
        if not (global_singleton.config.has_section(p.name) and \
                global_singleton.config.has_option(p.name, "enabled") and \
                global_singleton.config.get(p.name, "enabled") == "true"):
            break
        if p.requires_logging:
            # make sure the environment can accept a logfile by
            # creating the directory in the correct place,
            # and setting that in the plugin object; the plugin
            # itself will switch on its own logging when ready,
            # attaching a filehandler to the global log.
            plogsdir = os.path.join(os.path.dirname(
                global_singleton.config_location), "logs", p.name)
            if not os.path.exists(plogsdir):
                os.makedirs(plogsdir)
            p.set_log_dir(plogsdir)
Esempio n. 23
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)
def main():
    parser = OptionParser(
        usage='usage: %prog [options] [txid:n]',
        description=
        "Adds one or more utxos to the list that can be used to make "
        "commitments for anti-snooping. Note that this utxo, and its "
        "PUBkey, will be revealed to makers, so consider the privacy "
        "implication. "
        "It may be useful to those who are having trouble making "
        "coinjoins due to several unsuccessful attempts (especially "
        "if your joinmarket wallet is new). "
        "'Utxo' means unspent transaction output, it must not "
        "already be spent. "
        "The options -w, -r and -R offer ways to load these utxos "
        "from a file or wallet. "
        "If you enter a single utxo without these options, you will be "
        "prompted to enter the private key here - it must be in "
        "WIF compressed format. "
        "BE CAREFUL about handling private keys! "
        "Don't do this in insecure environments. "
        "Also note this ONLY works for standard p2wpkh (native segwit) "
        "or p2sh-p2wpkh (nested segwit) utxos.")
    add_base_options(parser)
    parser.add_option(
        '-r',
        '--read-from-file',
        action='store',
        type='str',
        dest='in_file',
        help=
        'name of plain text csv file containing utxos, one per line, format: '
        'txid:N, WIF-compressed-privkey')
    parser.add_option(
        '-R',
        '--read-from-json',
        action='store',
        type='str',
        dest='in_json',
        help=
        'name of json formatted file containing utxos with private keys, as '
        'output from "python wallet-tool.py -p walletname showutxos"')
    parser.add_option(
        '-w',
        '--load-wallet',
        action='store',
        type='str',
        dest='loadwallet',
        help='name of wallet from which to load utxos and use as commitments.')
    parser.add_option(
        '-g',
        '--gap-limit',
        action='store',
        type='int',
        dest='gaplimit',
        default=6,
        help=
        'Only to be used with -w; gap limit for Joinmarket wallet, default 6.')
    parser.add_option(
        '-M',
        '--max-mixdepth',
        action='store',
        type='int',
        dest='maxmixdepth',
        default=5,
        help=
        'Only to be used with -w; number of mixdepths for wallet, default 5.')
    parser.add_option(
        '-d',
        '--delete-external',
        action='store_true',
        dest='delete_ext',
        help='deletes the current list of external commitment utxos',
        default=False)
    parser.add_option(
        '-v',
        '--validate-utxos',
        action='store_true',
        dest='validate',
        help='validate the utxos and pubkeys provided against the blockchain',
        default=False)
    parser.add_option(
        '-o',
        '--validate-only',
        action='store_true',
        dest='vonly',
        help='only validate the provided utxos (file or command line), not add',
        default=False)
    (options, args) = parser.parse_args()
    load_program_config(config_path=options.datadir)
    #TODO; sort out "commit file location" global so this script can
    #run without this hardcoding:
    utxo_data = []
    if options.delete_ext:
        other = options.in_file or options.in_json or options.loadwallet
        if len(args) > 0 or other:
            if input("You have chosen to delete commitments, other arguments "
                     "will be ignored; continue? (y/n)") != 'y':
                jmprint("Quitting", "warning")
                sys.exit(EXIT_SUCCESS)
        c, e = get_podle_commitments()
        jmprint(pformat(e), "info")
        if input("You will remove the above commitments; are you sure? (y/n): "
                 ) != 'y':
            jmprint("Quitting", "warning")
            sys.exit(EXIT_SUCCESS)
        update_commitments(external_to_remove=e)
        jmprint("Commitments deleted.", "important")
        sys.exit(EXIT_SUCCESS)

    #Three options (-w, -r, -R) for loading utxo and privkey pairs from a wallet,
    #csv file or json file.
    if options.loadwallet:
        wallet_path = get_wallet_path(options.loadwallet)
        wallet = open_wallet(wallet_path, gap_limit=options.gaplimit)
        wallet_service = WalletService(wallet)
        if wallet_service.rpc_error:
            sys.exit(EXIT_FAILURE)
        while True:
            if wallet_service.sync_wallet(fast=not options.recoversync):
                break

        # minor note: adding a utxo from an external wallet for commitments, we
        # default to not allowing disabled utxos to avoid a privacy leak, so the
        # user would have to explicitly enable.
        for md, utxos in wallet_service.get_utxos_by_mixdepth().items():
            for utxo, utxodata in utxos.items():
                wif = wallet_service.get_wif_path(utxodata['path'])
                utxo_data.append((utxo, wif))

    elif options.in_file:
        with open(options.in_file, "rb") as f:
            utxo_info = f.readlines()
        for ul in utxo_info:
            ul = ul.rstrip()
            if ul:
                u, priv = get_utxo_info(ul.decode("utf-8"), utxo_binary=True)
                if not u:
                    quit(parser, "Failed to parse utxo info: " + str(ul))
                utxo_data.append((u, priv))
    elif options.in_json:
        if not os.path.isfile(options.in_json):
            jmprint("File: " + options.in_json + " not found.", "error")
            sys.exit(EXIT_FAILURE)
        with open(options.in_json, "rb") as f:
            try:
                utxo_json = json.loads(f.read())
            except:
                jmprint("Failed to read json from " + options.in_json, "error")
                sys.exit(EXIT_FAILURE)
        for u, pva in iteritems(utxo_json):
            utxobin, priv = get_utxo_info(",".join([u, pva["privkey"]]),
                                          utxo_binary=True)
            if not utxobin:
                quit(parser, "Failed to load utxo from json: " + str(u))
            utxo_data.append((utxobin, priv))
    elif len(args) == 1:
        ul = args[0]
        priv = input('input private key for ' + ul +
                     ', in WIF compressed format : ')
        u, priv = get_utxo_info(','.join([ul, priv]), utxo_binary=True)
        if not u:
            quit(parser, "Failed to parse utxo info: " + ul)
        utxo_data.append((u, priv))
    else:
        quit(parser, 'Invalid syntax')
    if options.validate or options.vonly:
        # if the utxos are loaded from a wallet, we use the wallet's
        # txtype to determine the value of `utxo_address_type`; if not,
        # we use joinmarket.cfg.
        if options.loadwallet:
            utxo_address_type = wallet_service.get_txtype()
        else:
            if jm_single().config.get("POLICY", "segwit") == "false":
                utxo_address_type = "p2pkh"
            elif jm_single().config.get("POLICY", "native") == "false":
                utxo_address_type = "p2sh-p2wpkh"
            else:
                utxo_address_type = "p2wpkh"
        if not validate_utxo_data(utxo_data,
                                  utxo_address_type=utxo_address_type):
            quit(parser, "Utxos did not validate, quitting")
    if options.vonly:
        sys.exit(EXIT_SUCCESS)

    #We are adding utxos to the external list
    assert len(utxo_data)
    add_ext_commitments(utxo_data)
Esempio n. 25
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(0)
    load_program_config()

    check_regtest()

    #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:]
    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(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)

    #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 cli_display_user_words(words, mnemonic_extension):
    text = 'Write down this wallet recovery mnemonic\n\n' + words + '\n'
    if mnemonic_extension:
        text += '\nAnd this mnemonic extension: ' + mnemonic_extension + '\n'
    jmprint(text, "important")
    elif method == "signmessage":
        return wallet_signmessage(wallet, options.hd_path, args[2])


#Testing (can port to test modules, TODO)
if __name__ == "__main__":
    if not test_bip32_pathparse():
        sys.exit(0)
    rootpath = "m/0"
    walletbranch = 0
    accounts = range(3)
    acctlist = []
    for a in accounts:
        branches = []
        for forchange in range(2):
            entries = []
            for i in range(4):
                entries.append(
                    WalletViewEntry(rootpath, a, forchange, i,
                                    "DUMMYADDRESS" + str(i + a),
                                    [i * 10000000, i * 10000000]))
            branches.append(
                WalletViewBranch(rootpath,
                                 a,
                                 forchange,
                                 branchentries=entries,
                                 xpub="xpubDUMMYXPUB" + str(a + forchange)))
        acctlist.append(WalletViewAccount(rootpath, a, branches=branches))
    wallet = WalletView(rootpath + "/" + str(walletbranch), accounts=acctlist)
    jmprint(wallet.serialize(), "success")
def wallet_fetch_history(wallet, options):
    # sort txes in a db because python can be really bad with large lists
    con = sqlite3.connect(":memory:")
    con.row_factory = dict_factory
    tx_db = con.cursor()
    tx_db.execute("CREATE TABLE transactions(txid TEXT, "
                  "blockhash TEXT, blocktime INTEGER);")
    jm_single().debug_silence[0] = True
    wallet_name = jm_single().bc_interface.get_wallet_name(wallet)
    buf = range(1000)
    t = 0
    while len(buf) == 1000:
        buf = jm_single().bc_interface.rpc('listtransactions',
                                           ["*", 1000, t, True])
        t += len(buf)
        tx_data = ((tx['txid'], tx['blockhash'], tx['blocktime']) for tx in buf
                   if 'txid' in tx and 'blockhash' in tx and 'blocktime' in tx)
        tx_db.executemany('INSERT INTO transactions VALUES(?, ?, ?);', tx_data)

    txes = tx_db.execute('SELECT DISTINCT txid, blockhash, blocktime '
                         'FROM transactions ORDER BY blocktime').fetchall()
    wallet_script_set = set(
        wallet.get_script_path(p) for p in wallet.yield_known_paths())

    def s():
        return ',' if options.csv else ' '

    def sat_to_str(sat):
        return '%.8f' % (sat / 1e8)

    def sat_to_str_p(sat):
        return '%+.8f' % (sat / 1e8)

    def skip_n1(v):
        return '% 2s' % (str(v)) if v != -1 else ' #'

    def skip_n1_btc(v):
        return sat_to_str(v) if v != -1 else '#' + ' ' * 10

    def print_row(index, time, tx_type, amount, delta, balance, cj_n,
                  miner_fees, utxo_count, mixdepth_src, mixdepth_dst, txid):
        data = [
            index,
            datetime.fromtimestamp(time).strftime("%Y-%m-%d %H:%M"), tx_type,
            sat_to_str(amount),
            sat_to_str_p(delta),
            sat_to_str(balance),
            skip_n1(cj_n),
            sat_to_str(miner_fees),
            '% 3d' % utxo_count,
            skip_n1(mixdepth_src),
            skip_n1(mixdepth_dst)
        ]
        if options.verbosity % 2 == 0: data += [txid]
        jmprint(s().join(map('"{}"'.format, data)), "info")

    field_names = [
        'tx#', 'timestamp', 'type', 'amount/btc', 'balance-change/btc',
        'balance/btc', 'coinjoin-n', 'total-fees', 'utxo-count',
        'mixdepth-from', 'mixdepth-to'
    ]
    if options.verbosity % 2 == 0: field_names += ['txid']
    if options.csv:
        jmprint('Bumping verbosity level to 4 due to --csv flag', "debug")
        options.verbosity = 4
    if options.verbosity > 0: jmprint(s().join(field_names), "info")
    if options.verbosity <= 2: cj_batch = [0] * 8 + [[]] * 2
    balance = 0
    utxo_count = 0
    deposits = []
    deposit_times = []
    tx_number = 0
    for tx in txes:
        is_coinjoin, cj_amount, cj_n, output_script_values, blocktime, txd =\
            get_tx_info(tx['txid'])

        our_output_scripts = wallet_script_set.intersection(
            output_script_values.keys())

        rpc_inputs = []
        for ins in txd['ins']:
            try:
                wallet_tx = jm_single().bc_interface.rpc(
                    'gettransaction', [ins['outpoint']['hash']])
            except JsonRpcError:
                continue
            input_dict = btc.deserialize(str(
                wallet_tx['hex']))['outs'][ins['outpoint']['index']]
            rpc_inputs.append(input_dict)

        rpc_input_scripts = set(
            binascii.unhexlify(ind['script']) for ind in rpc_inputs)
        our_input_scripts = wallet_script_set.intersection(rpc_input_scripts)
        our_input_values = [
            ind['value'] for ind in rpc_inputs
            if binascii.unhexlify(ind['script']) in our_input_scripts
        ]
        our_input_value = sum(our_input_values)
        utxos_consumed = len(our_input_values)

        tx_type = None
        amount = 0
        delta_balance = 0
        fees = 0
        mixdepth_src = -1
        mixdepth_dst = -1
        #TODO this seems to assume all the input addresses are from the same
        # mixdepth, which might not be true
        if len(our_input_scripts) == 0 and len(our_output_scripts) > 0:
            #payment to us
            amount = sum([output_script_values[a] for a in our_output_scripts])
            tx_type = 'deposit    '
            cj_n = -1
            delta_balance = amount
            mixdepth_dst = tuple(
                wallet.get_script_mixdepth(a) for a in our_output_scripts)
            if len(mixdepth_dst) == 1:
                mixdepth_dst = mixdepth_dst[0]
        elif len(our_input_scripts) == 0 and len(our_output_scripts) == 0:
            continue  # skip those that don't belong to our wallet
        elif len(our_input_scripts) > 0 and len(our_output_scripts) == 0:
            # we swept coins elsewhere
            if is_coinjoin:
                tx_type = 'cj sweepout'
                amount = cj_amount
                fees = our_input_value - cj_amount
            else:
                tx_type = 'sweep out  '
                amount = sum([v for v in output_script_values.values()])
                fees = our_input_value - amount
            delta_balance = -our_input_value
            mixdepth_src = wallet.get_script_mixdepth(
                list(our_input_scripts)[0])
        elif len(our_input_scripts) > 0 and len(our_output_scripts) == 1:
            # payment to somewhere with our change address getting the remaining
            change_value = output_script_values[list(our_output_scripts)[0]]
            if is_coinjoin:
                tx_type = 'cj withdraw'
                amount = cj_amount
            else:
                tx_type = 'withdraw   '
                #TODO does tx_fee go here? not my_tx_fee only?
                amount = our_input_value - change_value
                cj_n = -1
            delta_balance = change_value - our_input_value
            fees = our_input_value - change_value - cj_amount
            mixdepth_src = wallet.get_script_mixdepth(
                list(our_input_scripts)[0])
        elif len(our_input_scripts) > 0 and len(our_output_scripts) == 2:
            #payment to self
            out_value = sum(
                [output_script_values[a] for a in our_output_scripts])
            if not is_coinjoin:
                jmprint('this is wrong TODO handle non-coinjoin internal',
                        "warning")
            tx_type = 'cj internal'
            amount = cj_amount
            delta_balance = out_value - our_input_value
            mixdepth_src = wallet.get_script_mixdepth(
                list(our_input_scripts)[0])
            cj_script = list(
                set([
                    a for a, v in iteritems(output_script_values)
                    if v == cj_amount
                ]).intersection(our_output_scripts))[0]
            mixdepth_dst = wallet.get_script_mixdepth(cj_script)
        else:
            tx_type = 'unknown type'
            jmprint('our utxos: ' + str(len(our_input_scripts)) \
                  + ' in, ' + str(len(our_output_scripts)) + ' out')
        balance += delta_balance
        utxo_count += (len(our_output_scripts) - utxos_consumed)
        index = '%4d' % (tx_number)
        tx_number += 1
        if options.verbosity > 0:
            if options.verbosity <= 2:
                n = cj_batch[0]
                if tx_type == 'cj internal':
                    cj_batch[0] += 1
                    cj_batch[1] += blocktime
                    cj_batch[2] += amount
                    cj_batch[3] += delta_balance
                    cj_batch[4] = balance
                    cj_batch[5] += cj_n
                    cj_batch[6] += fees
                    cj_batch[7] += utxo_count
                    cj_batch[8] += [mixdepth_src]
                    cj_batch[9] += [mixdepth_dst]
                elif tx_type != 'unknown type':
                    if n > 0:
                        # print the previously-accumulated batch
                        print_row('N=' + "%2d" % n, cj_batch[1] / n,
                                  'cj batch   ', cj_batch[2], cj_batch[3],
                                  cj_batch[4], cj_batch[5] / n, cj_batch[6],
                                  cj_batch[7] / n, min(cj_batch[8]),
                                  max(cj_batch[9]), '...')
                    cj_batch = [0] * 8 + [[]] * 2  # reset the batch collector
                    # print batch terminating row
                    print_row(index, blocktime, tx_type, amount, delta_balance,
                              balance, cj_n, fees, utxo_count, mixdepth_src,
                              mixdepth_dst, tx['txid'])
            elif options.verbosity >= 5 or \
                 (options.verbosity >= 3 and tx_type != 'unknown type'):
                print_row(index, blocktime, tx_type, amount, delta_balance,
                          balance, cj_n, fees, utxo_count, mixdepth_src,
                          mixdepth_dst, tx['txid'])

        if tx_type != 'cj internal':
            deposits.append(delta_balance)
            deposit_times.append(blocktime)

    # we could have a leftover batch!
    if options.verbosity <= 2:
        n = cj_batch[0]
        if n > 0:
            print_row('N=' + "%2d" % n, cj_batch[1] / n, 'cj batch   ',
                      cj_batch[2], cj_batch[3], cj_batch[4], cj_batch[5] / n,
                      cj_batch[6], cj_batch[7] / n, min(cj_batch[8]),
                      max(cj_batch[9]), '...')

    bestblockhash = jm_single().bc_interface.rpc('getbestblockhash', [])
    try:
        #works with pruning enabled, but only after v0.12
        now = jm_single().bc_interface.rpc('getblockheader',
                                           [bestblockhash])['time']
    except JsonRpcError:
        now = jm_single().bc_interface.rpc('getblock', [bestblockhash])['time']
    jmprint('     %s best block is %s' %
            (datetime.fromtimestamp(now).strftime("%Y-%m-%d %H:%M"),
             bestblockhash))
    total_profit = float(balance - sum(deposits)) / float(100000000)
    jmprint('total profit = %.8f BTC' % total_profit)

    if abs(total_profit) > 0:
        try:
            # https://gist.github.com/chris-belcher/647da261ce718fc8ca10
            import numpy as np
            from scipy.optimize import brentq
            deposit_times = np.array(deposit_times)
            now -= deposit_times[0]
            deposit_times -= deposit_times[0]
            deposits = np.array(deposits)

            def f(r, deposits, deposit_times, now, final_balance):
                return np.sum(
                    np.exp((now - deposit_times) / 60.0 / 60 / 24 / 365)**r *
                    deposits) - final_balance

            r = brentq(f,
                       a=1,
                       b=-1,
                       args=(deposits, deposit_times, now, balance))
            jmprint(
                'continuously compounded equivalent annual interest rate = ' +
                str(r * 100) + ' %')
            jmprint('(as if yield generator was a bank account)')
        except ImportError:
            jmprint('scipy not installed, unable to predict accumulation rate')
            jmprint('to add it to this virtualenv, use `pip install scipy`')

    total_wallet_balance = sum(wallet.get_balance_by_mixdepth().values())
    if balance != total_wallet_balance:
        jmprint(
            ('BUG ERROR: wallet balance (%s) does not match balance from ' +
             'history (%s)') %
            (sat_to_str(total_wallet_balance), sat_to_str(balance)))
    wallet_utxo_count = sum(map(len, wallet.get_utxos_by_mixdepth_().values()))
    if utxo_count != wallet_utxo_count:
        jmprint((
            'BUG ERROR: wallet utxo count (%d) does not match utxo count from '
            + 'history (%s)') % (wallet_utxo_count, utxo_count))
    # wallet-tool.py prints return value, so return empty string instead of None here
    return ''