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()
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)
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")
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
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
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)
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")
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
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])
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)
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)
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 ''