def main(): parser = get_sendpayment_parser() (options, args) = parser.parse_args() load_program_config(config_path=options.datadir) if options.p2ep and len(args) != 3: parser.error("Joinmarket peer-to-peer PayJoin requires exactly three " "arguments: wallet, amount and destination address.") sys.exit(EXIT_ARGERROR) elif options.schedule == '': if ((len(args) < 2) or (btc.is_bip21_uri(args[1]) and len(args) != 2) or (not btc.is_bip21_uri(args[1]) and len(args) != 3)): parser.error("Joinmarket sendpayment (coinjoin) needs arguments:" " wallet, amount, destination address or wallet, bitcoin_uri.") sys.exit(EXIT_ARGERROR) #without schedule file option, use the arguments to create a schedule #of a single transaction sweeping = False bip78url = None if options.schedule == '': if btc.is_bip21_uri(args[1]): parsed = btc.decode_bip21_uri(args[1]) try: amount = parsed['amount'] except KeyError: parser.error("Given BIP21 URI does not contain amount.") sys.exit(EXIT_ARGERROR) destaddr = parsed['address'] if 'jmnick' in parsed: if "pj" in parsed: parser.error("Cannot specify both BIP78 and Joinmarket " "peer-to-peer payjoin at the same time!") sys.exit(EXIT_ARGERROR) options.p2ep = parsed['jmnick'] elif "pj" in parsed: # note that this is a URL; its validity # checking is deferred to twisted.web.client.Agent bip78url = parsed["pj"] # setting makercount only for fee sanity check. # note we ignore any user setting and enforce N=0, # as this is a flag in the code for a non-JM coinjoin; # for the fee sanity check, note that BIP78 currently # will only allow small fee changes, so N=0 won't # be very inaccurate. jmprint("Attempting to pay via payjoin.", "info") options.makercount = 0 else: amount = btc.amount_to_sat(args[1]) if amount == 0: sweeping = True destaddr = args[2] mixdepth = options.mixdepth addr_valid, errormsg = validate_address(destaddr) command_to_burn = (is_burn_destination(destaddr) and sweeping and options.makercount == 0 and not options.p2ep) if not addr_valid and not command_to_burn: jmprint('ERROR: Address invalid. ' + errormsg, "error") if is_burn_destination(destaddr): jmprint("The required options for burning coins are zero makers" + " (-N 0), sweeping (amount = 0) and not using P2EP", "info") sys.exit(EXIT_ARGERROR) if sweeping == False and amount < DUST_THRESHOLD: jmprint('ERROR: Amount ' + btc.amount_to_str(amount) + ' is below dust threshold ' + btc.amount_to_str(DUST_THRESHOLD) + '.', "error") sys.exit(EXIT_ARGERROR) if (options.makercount != 0 and options.makercount < jm_single().config.getint( "POLICY", "minimum_makers")): jmprint('ERROR: Maker count ' + str(options.makercount) + ' below minimum_makers (' + str(jm_single().config.getint( "POLICY", "minimum_makers")) + ') in joinmarket.cfg.', "error") sys.exit(EXIT_ARGERROR) schedule = [[options.mixdepth, amount, options.makercount, destaddr, 0.0, NO_ROUNDING, 0]] else: if btc.is_bip21_uri(args[1]): parser.error("Schedule files are not compatible with bip21 uris.") sys.exit(EXIT_ARGERROR) if options.p2ep: parser.error("Schedule files are not compatible with PayJoin") sys.exit(EXIT_FAILURE) result, schedule = get_schedule(options.schedule) if not result: log.error("Failed to load schedule file, quitting. Check the syntax.") log.error("Error was: " + str(schedule)) sys.exit(EXIT_FAILURE) mixdepth = 0 for s in schedule: if s[1] == 0: sweeping = True #only used for checking the maximum mixdepth required mixdepth = max([mixdepth, s[0]]) wallet_name = args[0] check_regtest() if options.pickorders: chooseOrdersFunc = pick_order if sweeping: jmprint('WARNING: You may have to pick offers multiple times', "warning") jmprint('WARNING: due to manual offer picking while sweeping', "warning") else: chooseOrdersFunc = options.order_choose_fn # If tx_fees are set manually by CLI argument, override joinmarket.cfg: if int(options.txfee) > 0: jm_single().config.set("POLICY", "tx_fees", str(options.txfee)) # Dynamically estimate a realistic fee. # At this point we do not know even the number of our own inputs, so # we guess conservatively with 2 inputs and 2 outputs each. fee_per_cp_guess = estimate_tx_fee(2, 2, txtype="p2sh-p2wpkh") log.debug("Estimated miner/tx fee for each cj participant: " + str( fee_per_cp_guess)) maxcjfee = (1, float('inf')) if not options.p2ep and not options.pickorders and \ options.makercount != 0: maxcjfee = get_max_cj_fee_values(jm_single().config, options) log.info("Using maximum coinjoin fee limits per maker of {:.4%}, {} " "".format(maxcjfee[0], btc.amount_to_str(maxcjfee[1]))) log.info('starting sendpayment') max_mix_depth = max([mixdepth, options.amtmixdepths - 1]) wallet_path = get_wallet_path(wallet_name, None) wallet = open_test_wallet_maybe( wallet_path, wallet_name, max_mix_depth, wallet_password_stdin=options.wallet_password_stdin, gap_limit=options.gaplimit) wallet_service = WalletService(wallet) # in this script, we need the wallet synced before # logic processing for some paths, so do it now: while not wallet_service.synced: wallet_service.sync_wallet(fast=not options.recoversync) # the sync call here will now be a no-op: wallet_service.startService() # From the estimated tx fees, check if the expected amount is a # significant value compared the the cj amount; currently enabled # only for single join (the predominant, non-advanced case) if options.schedule == '': total_cj_amount = amount if total_cj_amount == 0: total_cj_amount = wallet_service.get_balance_by_mixdepth()[options.mixdepth] if total_cj_amount == 0: raise ValueError("No confirmed coins in the selected mixdepth. Quitting") exp_tx_fees_ratio = ((1 + options.makercount) * fee_per_cp_guess) / total_cj_amount if exp_tx_fees_ratio > 0.05: jmprint('WARNING: Expected bitcoin network miner fees for this coinjoin' ' amount are roughly {:.1%}'.format(exp_tx_fees_ratio), "warning") if input('You might want to modify your tx_fee' ' settings in joinmarket.cfg. Still continue? (y/n):')[0] != 'y': sys.exit('Aborted by user.') else: log.info("Estimated miner/tx fees for this coinjoin amount: {:.1%}" .format(exp_tx_fees_ratio)) if options.makercount == 0 and not options.p2ep and not bip78url: tx = direct_send(wallet_service, amount, mixdepth, destaddr, options.answeryes, with_final_psbt=options.with_psbt) if options.with_psbt: log.info("This PSBT is fully signed and can be sent externally for " "broadcasting:") log.info(tx.to_base64()) return if wallet.get_txtype() == 'p2pkh': jmprint("Only direct sends (use -N 0) are supported for " "legacy (non-segwit) wallets.", "error") sys.exit(EXIT_ARGERROR) def filter_orders_callback(orders_fees, cjamount): orders, total_cj_fee = orders_fees log.info("Chose these orders: " +pprint.pformat(orders)) log.info('total cj fee = ' + str(total_cj_fee)) total_fee_pc = 1.0 * total_cj_fee / cjamount log.info('total coinjoin fee = ' + str(float('%.3g' % ( 100.0 * total_fee_pc))) + '%') WARNING_THRESHOLD = 0.02 # 2% if total_fee_pc > WARNING_THRESHOLD: log.info('\n'.join(['=' * 60] * 3)) log.info('WARNING ' * 6) log.info('\n'.join(['=' * 60] * 1)) log.info('OFFERED COINJOIN FEE IS UNUSUALLY HIGH. DOUBLE/TRIPLE CHECK.') log.info('\n'.join(['=' * 60] * 1)) log.info('WARNING ' * 6) log.info('\n'.join(['=' * 60] * 3)) if not options.answeryes: if input('send with these orders? (y/n):')[0] != 'y': return False return True def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None): if fromtx == "unconfirmed": #If final entry, stop *here*, don't wait for confirmation if taker.schedule_index + 1 == len(taker.schedule): reactor.stop() return if fromtx: if res: txd, txid = txdetails reactor.callLater(waittime*60, clientfactory.getClient().clientStart) else: #a transaction failed; we'll try to repeat without the #troublemakers. #If this error condition is reached from Phase 1 processing, #and there are less than minimum_makers honest responses, we #just give up (note that in tumbler we tweak and retry, but #for sendpayment the user is "online" and so can manually #try again). #However if the error is in Phase 2 and we have minimum_makers #or more responses, we do try to restart with the honest set, here. if taker.latest_tx is None: #can only happen with < minimum_makers; see above. log.info("A transaction failed but there are insufficient " "honest respondants to continue; giving up.") reactor.stop() return #This is Phase 2; do we have enough to try again? taker.add_honest_makers(list(set( taker.maker_utxo_data.keys()).symmetric_difference( set(taker.nonrespondants)))) if len(taker.honest_makers) < jm_single().config.getint( "POLICY", "minimum_makers"): log.info("Too few makers responded honestly; " "giving up this attempt.") reactor.stop() return jmprint("We failed to complete the transaction. The following " "makers responded honestly: " + str(taker.honest_makers) +\ ", so we will retry with them.", "warning") #Now we have to set the specific group we want to use, and hopefully #they will respond again as they showed honesty last time. #we must reset the number of counterparties, as well as fix who they #are; this is because the number is used to e.g. calculate fees. #cleanest way is to reset the number in the schedule before restart. taker.schedule[taker.schedule_index][2] = len(taker.honest_makers) log.info("Retrying with: " + str(taker.schedule[ taker.schedule_index][2]) + " counterparties.") #rewind to try again (index is incremented in Taker.initialize()) taker.schedule_index -= 1 taker.set_honest_only(True) reactor.callLater(5.0, clientfactory.getClient().clientStart) else: if not res: log.info("Did not complete successfully, shutting down") #Should usually be unreachable, unless conf received out of order; #because we should stop on 'unconfirmed' for last (see above) else: log.info("All transactions completed correctly") reactor.stop() if options.p2ep: # This workflow requires command line reading; we force info level logging # to remove noise, and mostly communicate to the user with the fn # log.info (directly or via default taker_info_callback). set_logging_level("INFO") # in the case where the payment just hangs for a long period, allow # it to fail gracefully with an information message; this is triggered # only by the stallMonitor, which gives up after 20*maker_timeout_sec: def p2ep_on_finished_callback(res, fromtx=False, waittime=0.0, txdetails=None): log.error("PayJoin payment was NOT made, timed out.") reactor.stop() taker = P2EPTaker(options.p2ep, wallet_service, schedule, callbacks=(None, None, p2ep_on_finished_callback)) elif bip78url: # TODO sanity check wallet type is segwit manager = parse_payjoin_setup(args[1], wallet_service, options.mixdepth) reactor.callWhenRunning(send_payjoin, manager) reactor.run() return else: taker = Taker(wallet_service, schedule, order_chooser=chooseOrdersFunc, max_cj_fee=maxcjfee, callbacks=(filter_orders_callback, None, taker_finished)) clientfactory = JMClientProtocolFactory(taker) nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False p2ep = True if options.p2ep != "" else False if jm_single().config.get("BLOCKCHAIN", "network") in ["regtest", "testnet"]: startLogging(sys.stdout) start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, daemon=daemon, p2ep=p2ep)
def test_start_ygs(setup_ygrunner, num_ygs, wallet_structures, mean_amt, malicious, deterministic): """Set up some wallets, for the ygs and 1 sp. Then start the ygs in background and publish the seed of the sp wallet for easy import into -qt """ if jm_single().config.get("POLICY", "native") == "true": walletclass = SegwitWallet else: # TODO add Legacy walletclass = SegwitLegacyWallet wallet_services = make_wallets(num_ygs + 1, wallet_structures=wallet_structures, mean_amt=mean_amt, walletclass=walletclass) #the sendpayment bot uses the last wallet in the list wallet_service = wallet_services[num_ygs]['wallet'] jmprint("\n\nTaker wallet seed : " + wallet_services[num_ygs]['seed']) # for manual audit if necessary, show the maker's wallet seeds # also (note this audit should be automated in future, see # test_full_coinjoin.py in this directory) jmprint("\n\nMaker wallet seeds: ") for i in range(num_ygs): jmprint("Maker seed: " + wallet_services[i]['seed']) jmprint("\n") wallet_service.sync_wallet(fast=True) ygclass = YieldGeneratorBasic # As per previous note, override non-default command line settings: options = {} for x in [ "ordertype", "txfee", "txfee_factor", "cjfee_a", "cjfee_r", "cjfee_factor", "minsize", "size_factor" ]: options[x] = jm_single().config.get("YIELDGENERATOR", x) ordertype = options["ordertype"] txfee = int(options["txfee"]) txfee_factor = float(options["txfee_factor"]) cjfee_factor = float(options["cjfee_factor"]) size_factor = float(options["size_factor"]) if ordertype == 'reloffer': cjfee_r = options["cjfee_r"] # minimum size is such that you always net profit at least 20% #of the miner fee minsize = max(int(1.2 * txfee / float(cjfee_r)), int(options["minsize"])) cjfee_a = None elif ordertype == 'absoffer': cjfee_a = int(options["cjfee_a"]) minsize = int(options["minsize"]) cjfee_r = None else: assert False, "incorrect offertype config for yieldgenerator." txtype = wallet_service.get_txtype() if txtype == "p2wpkh": prefix = "sw0" elif txtype == "p2sh-p2wpkh": prefix = "sw" elif txtype == "p2pkh": prefix = "" else: assert False, "Unsupported wallet type for yieldgenerator: " + txtype ordertype = prefix + ordertype if malicious: if deterministic: ygclass = DeterministicMaliciousYieldGenerator else: ygclass = MaliciousYieldGenerator for i in range(num_ygs): cfg = [ txfee, cjfee_a, cjfee_r, ordertype, minsize, txfee_factor, cjfee_factor, size_factor ] wallet_service_yg = wallet_services[i]["wallet"] wallet_service_yg.startService() yg = ygclass(wallet_service_yg, cfg) if malicious: yg.set_maliciousness(malicious, mtype="tx") clientfactory = JMClientProtocolFactory(yg, proto_type="MAKER") if jm_single().config.get("SNICKER", "enabled") == "true": snicker_r = SNICKERReceiver(wallet_service_yg) servers = jm_single().config.get("SNICKER", "servers").split(",") snicker_factory = SNICKERClientProtocolFactory(snicker_r, servers) else: snicker_factory = None nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False rs = True if i == num_ygs - 1 else False start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, snickerfactory=snicker_factory, daemon=daemon, rs=rs)
def ygmain(ygclass, txfee=1000, cjfee_a=200, cjfee_r=0.002, ordertype='reloffer', nickserv_password='', minsize=100000, gaplimit=6): import sys parser = OptionParser(usage='usage: %prog [options] [wallet file]') add_base_options(parser) parser.add_option('-o', '--ordertype', action='store', type='string', dest='ordertype', default=ordertype, help='type of order; can be either reloffer or absoffer') parser.add_option('-t', '--txfee', action='store', type='int', dest='txfee', default=txfee, help='minimum miner fee in satoshis') parser.add_option('-c', '--cjfee', action='store', type='string', dest='cjfee', default='', help='requested coinjoin fee in satoshis or proportion') parser.add_option('-p', '--password', action='store', type='string', dest='password', default=nickserv_password, help='irc nickserv password') parser.add_option('-s', '--minsize', action='store', type='int', dest='minsize', default=minsize, help='minimum coinjoin size in satoshis') parser.add_option('-g', '--gap-limit', action='store', type="int", dest='gaplimit', default=gaplimit, help='gap limit for wallet, default=' + str(gaplimit)) parser.add_option('-m', '--mixdepth', action='store', type='int', dest='mixdepth', default=None, help="highest mixdepth to use") (options, args) = parser.parse_args() if len(args) < 1: parser.error('Needs a wallet') sys.exit(EXIT_ARGERROR) wallet_name = args[0] ordertype = options.ordertype txfee = options.txfee if ordertype == 'reloffer': if options.cjfee != '': cjfee_r = options.cjfee # minimum size is such that you always net profit at least 20% #of the miner fee minsize = max(int(1.2 * txfee / float(cjfee_r)), options.minsize) elif ordertype == 'absoffer': if options.cjfee != '': cjfee_a = int(options.cjfee) minsize = options.minsize else: parser.error('You specified an incorrect offer type which ' +\ 'can be either reloffer or absoffer') sys.exit(EXIT_ARGERROR) nickserv_password = options.password load_program_config(config_path=options.datadir) if jm_single().bc_interface is None: jlog.error("Running yield generator requires configured " + "blockchain source.") sys.exit(EXIT_FAILURE) wallet_path = get_wallet_path(wallet_name, None) wallet = open_test_wallet_maybe( wallet_path, wallet_name, options.mixdepth, wallet_password_stdin=options.wallet_password_stdin, gap_limit=options.gaplimit) wallet_service = WalletService(wallet) while not wallet_service.synced: wallet_service.sync_wallet(fast=not options.recoversync) wallet_service.startService() txtype = wallet_service.get_txtype() if txtype == "p2wpkh": prefix = "sw0" elif txtype == "p2sh-p2wpkh": prefix = "sw" elif txtype == "p2pkh": prefix = "" else: jlog.error("Unsupported wallet type for yieldgenerator: " + txtype) sys.exit(EXIT_ARGERROR) ordertype = prefix + ordertype jlog.debug("Set the offer type string to: " + ordertype) maker = ygclass( wallet_service, [options.txfee, cjfee_a, cjfee_r, ordertype, options.minsize]) jlog.info('starting yield generator') clientfactory = JMClientProtocolFactory(maker, proto_type="MAKER") nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False if jm_single().config.get("BLOCKCHAIN", "network") in ["regtest", "testnet"]: startLogging(sys.stdout) start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, daemon=daemon)
def test_cj(setup_full_coinjoin, num_ygs, wallet_structures, mean_amt, malicious, deterministic): """Starts by setting up wallets for maker and taker bots; then, instantiates a single taker with the final wallet. The remaining wallets are used to set up YieldGenerators (basic form). All the wallets are given coins according to the rules of make_wallets, using the parameters for the values. The final start_reactor call is the only one that actually starts the reactor; the others only set up protocol instances. Inline are custom callbacks for the Taker, and these are basically copies of those in the `sendpayment.py` script for now, but they could be customized later for testing. The Taker's schedule is a single coinjoin, using basically random values, again this could be easily edited or parametrized if we feel like it. """ # Set up some wallets, for the ygs and 1 sp. wallets = make_wallets(num_ygs + 1, wallet_structures=wallet_structures, mean_amt=mean_amt) #the sendpayment bot uses the last wallet in the list wallet = wallets[num_ygs]['wallet'] sync_wallet(wallet, fast=True) # grab a dest addr from the wallet destaddr = wallet.get_external_addr(4) coinjoin_amt = 20000000 schedule = [[1, coinjoin_amt, 2, destaddr, 0.0, False]] """ The following two callback functions are as simple as possible modifications of the same in scripts/sendpayment.py """ def filter_orders_callback(orders_fees, cjamount): return True def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None): def final_checks(): sync_wallet(wallet, fast=True) newbal = wallet.get_balance_by_mixdepth()[4] oldbal = wallet.get_balance_by_mixdepth()[1] # These are our check that the coinjoin succeeded assert newbal == coinjoin_amt # TODO: parametrize these; cj fees = 38K (.001 x 20M x 2 makers) # minus 1K tx fee contribution each; 600M is original balance # in mixdepth 1 assert oldbal + newbal + (40000 - 2000) + taker.total_txfee == 600000000 if fromtx == "unconfirmed": #If final entry, stop *here*, don't wait for confirmation if taker.schedule_index + 1 == len(taker.schedule): reactor.stop() final_checks() return if fromtx: # currently this test uses a schedule with only one entry assert False, "taker_finished was called with fromtx=True" reactor.stop() return else: if not res: assert False, "Did not complete successfully, shutting down" # Note that this is required in both conditional branches, # especially in testing, because it's possible to receive the # confirmed callback before the unconfirmed. reactor.stop() final_checks() # twisted logging is required for debugging: startLogging(sys.stdout) taker = Taker(wallet, schedule, order_chooser=random_under_max_order_choose, max_cj_fee=(0.1, 200), callbacks=(filter_orders_callback, None, taker_finished)) clientfactory = JMClientProtocolFactory(taker) nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, daemon=daemon, rs=False) txfee = 1000 cjfee_a = 4200 cjfee_r = '0.001' ordertype = 'swreloffer' minsize = 100000 ygclass = YieldGeneratorBasic # As noted above, this is not currently used but can be in future: if malicious or deterministic: raise NotImplementedError for i in range(num_ygs): cfg = [txfee, cjfee_a, cjfee_r, ordertype, minsize] sync_wallet(wallets[i]["wallet"], fast=True) yg = ygclass(wallets[i]["wallet"], cfg) if malicious: yg.set_maliciousness(malicious, mtype="tx") clientfactory = JMClientProtocolFactory(yg, proto_type="MAKER") nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False # As noted above, only the final start_reactor() call will # actually start it! rs = True if i == num_ygs - 1 else False start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, daemon=daemon, rs=rs)
def main(): parser = get_sendpayment_parser() (options, args) = parser.parse_args() load_program_config() walletclass = SegwitWallet if jm_single().config.get( "POLICY", "segwit") == "true" else Wallet if options.schedule == '' and len(args) < 3: parser.error('Needs a wallet, amount and destination address') sys.exit(0) #without schedule file option, use the arguments to create a schedule #of a single transaction sweeping = False if options.schedule == '': #note that sendpayment doesn't support fractional amounts, fractions throw #here. amount = int(args[1]) if amount == 0: sweeping = True destaddr = args[2] mixdepth = options.mixdepth addr_valid, errormsg = validate_address(destaddr) if not addr_valid: print('ERROR: Address invalid. ' + errormsg) return schedule = [[ options.mixdepth, amount, options.makercount, destaddr, 0.0, 0 ]] else: result, schedule = get_schedule(options.schedule) if not result: log.info( "Failed to load schedule file, quitting. Check the syntax.") log.info("Error was: " + str(schedule)) sys.exit(0) mixdepth = 0 for s in schedule: if s[1] == 0: sweeping = True #only used for checking the maximum mixdepth required mixdepth = max([mixdepth, s[0]]) wallet_name = args[0] #to allow testing of confirm/unconfirm callback for multiple txs if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface): jm_single().bc_interface.tick_forward_chain_interval = 10 jm_single().bc_interface.simulating = True jm_single().maker_timeout_sec = 15 chooseOrdersFunc = None if options.pickorders: chooseOrdersFunc = pick_order if sweeping: print('WARNING: You may have to pick offers multiple times') print('WARNING: due to manual offer picking while sweeping') elif options.choosecheapest: chooseOrdersFunc = cheapest_order_choose else: # choose randomly (weighted) chooseOrdersFunc = weighted_order_choose # Dynamically estimate a realistic fee if it currently is the default value. # At this point we do not know even the number of our own inputs, so # we guess conservatively with 2 inputs and 2 outputs each. if options.txfee == -1: options.txfee = max(options.txfee, estimate_tx_fee(2, 2, txtype="p2sh-p2wpkh")) log.debug("Estimated miner/tx fee for each cj participant: " + str(options.txfee)) assert (options.txfee >= 0) log.debug('starting sendpayment') if not options.userpcwallet: #maxmixdepth in the wallet is actually the *number* of mixdepths (so misnamed); #to ensure we have enough, must be at least (requested index+1) max_mix_depth = max([mixdepth + 1, options.amtmixdepths]) if not os.path.exists(os.path.join('wallets', wallet_name)): wallet = walletclass(wallet_name, None, max_mix_depth, options.gaplimit) else: while True: try: pwd = get_password("Enter wallet decryption passphrase: ") wallet = walletclass(wallet_name, pwd, max_mix_depth, options.gaplimit) except WalletError: print("Wrong password, try again.") continue except Exception as e: print("Failed to load wallet, error message: " + repr(e)) sys.exit(0) break else: wallet = BitcoinCoreWallet(fromaccount=wallet_name) if jm_single().config.get( "BLOCKCHAIN", "blockchain_source" ) == "electrum-server" and options.makercount != 0: jm_single().bc_interface.synctype = "with-script" #wallet sync will now only occur on reactor start if we're joining. sync_wallet(wallet, fast=options.fastsync) if options.makercount == 0: if isinstance(wallet, BitcoinCoreWallet): raise NotImplementedError( "Direct send only supported for JM wallets") direct_send(wallet, amount, mixdepth, destaddr, options.answeryes) return if walletclass == Wallet: print("Only direct sends (use -N 0) are supported for " "legacy (non-segwit) wallets.") return def filter_orders_callback(orders_fees, cjamount): orders, total_cj_fee = orders_fees log.info("Chose these orders: " + pprint.pformat(orders)) log.info('total cj fee = ' + str(total_cj_fee)) total_fee_pc = 1.0 * total_cj_fee / cjamount log.info('total coinjoin fee = ' + str(float('%.3g' % (100.0 * total_fee_pc))) + '%') WARNING_THRESHOLD = 0.02 # 2% if total_fee_pc > WARNING_THRESHOLD: log.info('\n'.join(['=' * 60] * 3)) log.info('WARNING ' * 6) log.info('\n'.join(['=' * 60] * 1)) log.info( 'OFFERED COINJOIN FEE IS UNUSUALLY HIGH. DOUBLE/TRIPLE CHECK.') log.info('\n'.join(['=' * 60] * 1)) log.info('WARNING ' * 6) log.info('\n'.join(['=' * 60] * 3)) if not options.answeryes: if raw_input('send with these orders? (y/n):')[0] != 'y': return False return True def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None): if fromtx == "unconfirmed": #If final entry, stop *here*, don't wait for confirmation if taker.schedule_index + 1 == len(taker.schedule): reactor.stop() return if fromtx: if res: txd, txid = txdetails taker.wallet.remove_old_utxos(txd) taker.wallet.add_new_utxos(txd, txid) reactor.callLater(waittime * 60, clientfactory.getClient().clientStart) else: #a transaction failed; just stop reactor.stop() else: if not res: log.info("Did not complete successfully, shutting down") #Should usually be unreachable, unless conf received out of order; #because we should stop on 'unconfirmed' for last (see above) else: log.info("All transactions completed correctly") reactor.stop() taker = Taker(wallet, schedule, order_chooser=chooseOrdersFunc, callbacks=(filter_orders_callback, None, taker_finished)) clientfactory = JMClientProtocolFactory(taker) nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False if jm_single().config.get("BLOCKCHAIN", "network") in ["regtest", "testnet"]: startLogging(sys.stdout) start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, daemon=daemon)
def receive_payjoin_main(makerclass): parser = OptionParser(usage='usage: %prog [options] [wallet file] [amount-to-receive]') parser.add_option('-g', '--gap-limit', action='store', type="int", dest='gaplimit', default=6, help='gap limit for wallet, default=6') parser.add_option('--recoversync', action='store_true', dest='recoversync', default=False, help=('choose to do detailed wallet sync, ' 'used for recovering on new Core instance.')) parser.add_option('-m', '--mixdepth', action='store', type='int', dest='mixdepth', default=0, help="mixdepth to source coins from") parser.add_option('-a', '--amtmixdepths', action='store', type='int', dest='amtmixdepths', help='number of mixdepths in wallet, default 5', default=5) parser.add_option('--wallet-password-stdin', action='store_true', default=False, dest='wallet_password_stdin', help='Read wallet password from stdin') (options, args) = parser.parse_args() if len(args) < 2: parser.error('Needs a wallet, and a receiving amount in bitcoins or satoshis') sys.exit(EXIT_ARGERROR) wallet_name = args[0] try: receiving_amount = amount_to_sat(args[1]) except: parser.error("Invalid receiving amount passed: " + receiving_amount) sys.exit(EXIT_FAILURE) if receiving_amount < 0: parser.error("Receiving amount must be a positive number") sys.exit(EXIT_FAILURE) load_program_config() check_regtest() # This workflow requires command line reading; we force info level logging # to remove noise, and mostly communicate to the user with the fn # log.info (via P2EPMaker.user_info). set_logging_level("INFO") wallet_path = get_wallet_path(wallet_name, 'wallets') max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1]) wallet = open_test_wallet_maybe( wallet_path, wallet_name, max_mix_depth, wallet_password_stdin=options.wallet_password_stdin, gap_limit=options.gaplimit) wallet_service = WalletService(wallet) while not wallet_service.synced: wallet_service.sync_wallet(fast=not options.recoversync) wallet_service.startService() # having enforced wallet sync, we can check if we have coins # to do payjoin in the mixdepth if wallet_service.get_balance_by_mixdepth()[options.mixdepth] == 0: jlog.error("Cannot do payjoin from mixdepth " + str( options.mixdepth) + ", no coins. Shutting down.") sys.exit(EXIT_ARGERROR) maker = makerclass(wallet_service, options.mixdepth, receiving_amount) jlog.info('starting receive-payjoin') clientfactory = JMClientProtocolFactory(maker, proto_type="MAKER") nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False if jm_single().config.get("BLOCKCHAIN", "network") in ["regtest", "testnet"]: startLogging(sys.stdout) start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, daemon=daemon, p2ep=True)
def main(): (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']) logsdir = os.path.join(os.path.dirname( jm_single().config_location), "logs") tumble_log = get_tumble_log(logsdir) 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") == "regtest": startLogging(sys.stdout) start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, daemon=daemon)
def receive_payjoin_main(makerclass): parser = OptionParser(usage='usage: %prog [options] [wallet file] [amount-to-receive]') parser.add_option('-g', '--gap-limit', action='store', type="int", dest='gaplimit', default=6, help='gap limit for wallet, default=6') parser.add_option('--fast', action='store_true', dest='fastsync', default=False, help=('choose to do fast wallet sync, only for Core and ' 'only for previously synced wallet')) parser.add_option('-m', '--mixdepth', action='store', type='int', dest='mixdepth', default=0, help="mixdepth to source coins from") parser.add_option('-a', '--amtmixdepths', action='store', type='int', dest='amtmixdepths', help='number of mixdepths in wallet, default 5', default=5) (options, args) = parser.parse_args() if len(args) < 2: parser.error('Needs a wallet, and a receiving amount in satoshis') sys.exit(0) wallet_name = args[0] try: receiving_amount = int(args[1]) except: parser.error("Invalid receiving amount passed: " + receiving_amount) sys.exit(0) if receiving_amount < 0: parser.error("Receiving amount must be a positive integer in satoshis") sys.exit(0) load_program_config() check_regtest() # This workflow requires command line reading; we force info level logging # to remove noise, and mostly communicate to the user with the fn # log.info (via P2EPMaker.user_info). set_logging_level("INFO") wallet_path = get_wallet_path(wallet_name, 'wallets') max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1]) wallet = open_test_wallet_maybe( wallet_path, wallet_name, max_mix_depth, gap_limit=options.gaplimit) if jm_single().config.get("BLOCKCHAIN", "blockchain_source") == "electrum-server": jm_single().bc_interface.synctype = "with-script" while not jm_single().bc_interface.wallet_synced: sync_wallet(wallet, fast=options.fastsync) maker = makerclass(wallet, options.mixdepth, receiving_amount) jlog.info('starting receive-payjoin') clientfactory = JMClientProtocolFactory(maker, proto_type="MAKER") nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False if jm_single().config.get("BLOCKCHAIN", "network") in ["regtest", "testnet"]: startLogging(sys.stdout) start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, daemon=daemon, p2ep=True)
def main(): tumble_log = get_tumble_log(logsdir) (options, args) = get_tumbler_parser().parse_args() options = vars(options) if len(args) < 1: parser.error('Needs a wallet file') sys.exit(0) load_program_config() #Load the wallet wallet_name = args[0] max_mix_depth = options['mixdepthsrc'] + options['mixdepthcount'] if not os.path.exists(os.path.join('wallets', wallet_name)): wallet = get_wallet_cls()(wallet_name, None, max_mix_depth) else: while True: try: pwd = get_password("Enter wallet decryption passphrase: ") wallet = get_wallet_cls()(wallet_name, pwd, max_mix_depth) except WalletError: print("Wrong password, try again.") continue except Exception as e: print("Failed to load wallet, error message: " + repr(e)) sys.exit(0) break if jm_single().config.get("BLOCKCHAIN", "blockchain_source") == "electrum-server": jm_single().bc_interface.synctype = "with-script" sync_wallet(wallet, fast=options['fastsync']) #Parse options and generate schedule #Output information to log files jm_single().mincjamount = options['mincjamount'] destaddrs = args[1:] print(destaddrs) #If the --restart flag is set we read the schedule #from the file, and filter out entries that are #already complete if options['restart']: res, schedule = get_schedule( os.path.join(logsdir, options['schedulefile'])) if not res: print("Failed to load schedule, name: " + str(options['schedulefile'])) print("Error was: " + str(schedule)) sys.exit(0) #This removes all entries that are marked as done schedule = [s for s in schedule if s[5] != 1] if isinstance(schedule[0][5], str) and len(schedule[0][5]) == 64: #ensure last transaction is confirmed before restart tumble_log.info("WAITING TO RESTART...") txid = schedule[0][5] restart_waiter(txid + ":0") #add 0 index because all have it #remove the already-done entry (this connects to the other TODO, #probably better *not* to truncate the done-already txs from file, #but simplest for now. schedule = schedule[1:] elif schedule[0][5] != 0: print("Error: first schedule entry is invalid.") sys.exit(0) with open(os.path.join(logsdir, options['schedulefile']), "wb") as f: f.write(schedule_to_text(schedule)) tumble_log.info("TUMBLE RESTARTING") else: #Create a new schedule from scratch schedule = get_tumble_schedule(options, destaddrs) tumble_log.info("TUMBLE STARTING") with open(os.path.join(logsdir, options['schedulefile']), "wb") as f: f.write(schedule_to_text(schedule)) print("Schedule written to logs/" + options['schedulefile']) tumble_log.info("With this schedule: ") tumble_log.info(pprint.pformat(schedule)) print("Progress logging to logs/TUMBLE.log") def filter_orders_callback(orders_fees, cjamount): """Decide whether to accept fees """ return tumbler_filter_orders_callback(orders_fees, cjamount, taker, options) def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None): """on_finished_callback for tumbler; processing is almost entirely deferred to generic taker_finished in tumbler_support module, except here reactor signalling. """ sfile = os.path.join(logsdir, options['schedulefile']) tumbler_taker_finished_update(taker, sfile, tumble_log, options, res, fromtx, waittime, txdetails) if not fromtx: reactor.stop() elif fromtx != "unconfirmed": reactor.callLater(waittime * 60, clientfactory.getClient().clientStart) #to allow testing of confirm/unconfirm callback for multiple txs if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface): jm_single().bc_interface.tick_forward_chain_interval = 10 jm_single().bc_interface.simulating = True jm_single().maker_timeout_sec = 15 #instantiate Taker with given schedule and run taker = Taker(wallet, schedule, order_chooser=weighted_order_choose, callbacks=(filter_orders_callback, None, taker_finished), tdestaddrs=destaddrs) clientfactory = JMClientProtocolFactory(taker) nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False if jm_single().config.get("BLOCKCHAIN", "network") in ["regtest", "testnet"]: startLogging(sys.stdout) start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, daemon=daemon)
def main(): tumble_log = get_tumble_log(logsdir) (options, args) = get_tumbler_parser().parse_args() options_org = options options = vars(options) if len(args) < 1: 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 ygmain(ygclass, nickserv_password='', gaplimit=6): import sys parser = OptionParser(usage='usage: %prog [options] [wallet file]') add_base_options(parser) # A note about defaults: # We want command line settings to override config settings. # This would naturally mean setting `default=` arguments here, to the # values in the config. # However, we cannot load the config until we know the datadir. # The datadir is a setting in the command line options, so we have to # call parser.parse_args() before we know the datadir. # Hence we do the following: set all modifyable-by-config arguments to # default "None" initially; call parse_args(); then call load_program_config # and override values of "None" with what is set in the config. # (remember, the joinmarket defaultconfig always sets every value, even if # the user doesn't). parser.add_option('-o', '--ordertype', action='store', type='string', dest='ordertype', default=None, help='type of order; can be either reloffer or absoffer') parser.add_option('-t', '--txfee', action='store', type='int', dest='txfee', default=None, help='minimum miner fee in satoshis') parser.add_option('-f', '--txfee-factor', action='store', type='float', dest='txfee_factor', default=None, help='variance around the average fee, decimal fraction') parser.add_option('-a', '--cjfee-a', action='store', type='string', dest='cjfee_a', default=None, help='requested coinjoin fee (absolute) in satoshis') parser.add_option('-r', '--cjfee-r', action='store', type='string', dest='cjfee_r', default=None, help='requested coinjoin fee (relative) as a decimal') parser.add_option('-j', '--cjfee-factor', action='store', type='float', dest='cjfee_factor', default=None, help='variance around the average fee, decimal fraction') parser.add_option('-p', '--password', action='store', type='string', dest='password', default=nickserv_password, help='irc nickserv password') parser.add_option('-s', '--minsize', action='store', type='int', dest='minsize', default=None, help='minimum coinjoin size in satoshis') parser.add_option('-z', '--size-factor', action='store', type='float', dest='size_factor', default=None, help='variance around all offer sizes, decimal fraction') parser.add_option('-g', '--gap-limit', action='store', type="int", dest='gaplimit', default=gaplimit, help='gap limit for wallet, default=' + str(gaplimit)) parser.add_option('-m', '--mixdepth', action='store', type='int', dest='mixdepth', default=None, help="highest mixdepth to use") (options, args) = parser.parse_args() # for string access, convert to dict: options = vars(options) if len(args) < 1: parser.error('Needs a wallet') sys.exit(EXIT_ARGERROR) load_program_config(config_path=options["datadir"]) # As per previous note, override non-default command line settings: for x in [ "ordertype", "txfee", "txfee_factor", "cjfee_a", "cjfee_r", "cjfee_factor", "minsize", "size_factor" ]: if options[x] is None: options[x] = jm_single().config.get("YIELDGENERATOR", x) wallet_name = args[0] ordertype = options["ordertype"] txfee = int(options["txfee"]) txfee_factor = float(options["txfee_factor"]) cjfee_factor = float(options["cjfee_factor"]) size_factor = float(options["size_factor"]) if ordertype == 'reloffer': cjfee_r = options["cjfee_r"] # minimum size is such that you always net profit at least 20% #of the miner fee minsize = max(int(1.2 * txfee / float(cjfee_r)), int(options["minsize"])) cjfee_a = None elif ordertype == 'absoffer': cjfee_a = int(options["cjfee_a"]) minsize = int(options["minsize"]) cjfee_r = None else: parser.error('You specified an incorrect offer type which ' +\ 'can be either reloffer or absoffer') sys.exit(EXIT_ARGERROR) nickserv_password = options["password"] if jm_single().bc_interface is None: jlog.error("Running yield generator requires configured " + "blockchain source.") sys.exit(EXIT_FAILURE) wallet_path = get_wallet_path(wallet_name, None) wallet = open_test_wallet_maybe( wallet_path, wallet_name, options["mixdepth"], wallet_password_stdin=options["wallet_password_stdin"], gap_limit=options["gaplimit"]) wallet_service = WalletService(wallet) while not wallet_service.synced: wallet_service.sync_wallet(fast=not options["recoversync"]) wallet_service.startService() txtype = wallet_service.get_txtype() if txtype == "p2wpkh": prefix = "sw0" elif txtype == "p2sh-p2wpkh": prefix = "sw" elif txtype == "p2pkh": prefix = "" else: jlog.error("Unsupported wallet type for yieldgenerator: " + txtype) sys.exit(EXIT_ARGERROR) ordertype = prefix + ordertype jlog.debug("Set the offer type string to: " + ordertype) maker = ygclass(wallet_service, [ txfee, cjfee_a, cjfee_r, ordertype, minsize, txfee_factor, cjfee_factor, size_factor ]) jlog.info('starting yield generator') clientfactory = JMClientProtocolFactory(maker, proto_type="MAKER") if jm_single().config.get("SNICKER", "enabled") == "true": if jm_single().config.get("BLOCKCHAIN", "network") == "mainnet": jlog.error("You have enabled SNICKER on mainnet, this is not " "yet supported for yieldgenerators; either use " "signet/regtest/testnet, or run SNICKER manually " "with snicker/receive-snicker.py.") sys.exit(EXIT_ARGERROR) snicker_r = SNICKERReceiver(wallet_service) servers = jm_single().config.get("SNICKER", "servers").split(",") snicker_factory = SNICKERClientProtocolFactory(snicker_r, servers) else: snicker_factory = None nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False if jm_single().config.get("BLOCKCHAIN", "network") in ["regtest", "testnet", "signet"]: startLogging(sys.stdout) start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, snickerfactory=snicker_factory, daemon=daemon)
def setUp(self): load_test_config() factory = JMClientProtocolFactory(DummyMaker(), proto_type='MAKER') self.client = factory.buildProtocol(None) self.tr = proto_helpers.StringTransport() self.client.makeConnection(self.tr)
def ygmain(ygclass, txfee=1000, cjfee_a=200, cjfee_r=0.002, ordertype='swreloffer', nickserv_password='', minsize=100000, gaplimit=6): import sys parser = OptionParser(usage='usage: %prog [options] [wallet file]') parser.add_option('-o', '--ordertype', action='store', type='string', dest='ordertype', default=ordertype, help='type of order; can be either reloffer or absoffer') parser.add_option('-t', '--txfee', action='store', type='int', dest='txfee', default=txfee, help='minimum miner fee in satoshis') parser.add_option('-c', '--cjfee', action='store', type='string', dest='cjfee', default='', help='requested coinjoin fee in satoshis or proportion') parser.add_option('-p', '--password', action='store', type='string', dest='password', default=nickserv_password, help='irc nickserv password') parser.add_option('-s', '--minsize', action='store', type='int', dest='minsize', default=minsize, help='minimum coinjoin size in satoshis') parser.add_option('-g', '--gap-limit', action='store', type="int", dest='gaplimit', default=gaplimit, help='gap limit for wallet, default=' + str(gaplimit)) parser.add_option('--fast', action='store_true', dest='fastsync', default=False, help=('choose to do fast wallet sync, only for Core and ' 'only for previously synced wallet')) parser.add_option('-m', '--mixdepth', action='store', type='int', dest='mixdepth', default=None, help="highest mixdepth to use") (options, args) = parser.parse_args() if len(args) < 1: parser.error('Needs a wallet') sys.exit(0) wallet_name = args[0] ordertype = options.ordertype txfee = options.txfee if ordertype in ('reloffer', 'swreloffer'): if options.cjfee != '': cjfee_r = options.cjfee # minimum size is such that you always net profit at least 20% #of the miner fee minsize = max(int(1.2 * txfee / float(cjfee_r)), options.minsize) elif ordertype in ('absoffer', 'swabsoffer'): if options.cjfee != '': cjfee_a = int(options.cjfee) minsize = options.minsize else: parser.error('You specified an incorrect offer type which ' +\ 'can be either swreloffer or swabsoffer') sys.exit(0) nickserv_password = options.password load_program_config() wallet_path = get_wallet_path(wallet_name, 'wallets') wallet = open_test_wallet_maybe(wallet_path, wallet_name, options.mixdepth, gap_limit=options.gaplimit) if jm_single().config.get("BLOCKCHAIN", "blockchain_source") == "electrum-server": jm_single().bc_interface.synctype = "with-script" while not jm_single().bc_interface.wallet_synced: sync_wallet(wallet, fast=options.fastsync) maker = ygclass( wallet, [options.txfee, cjfee_a, cjfee_r, options.ordertype, options.minsize]) jlog.info('starting yield generator') clientfactory = JMClientProtocolFactory(maker, proto_type="MAKER") nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False if jm_single().config.get("BLOCKCHAIN", "network") in ["regtest", "testnet"]: startLogging(sys.stdout) start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, daemon=daemon)
def main(): parser = get_sendpayment_parser() (options, args) = parser.parse_args() load_program_config() if options.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) if not options.p2ep 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) 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 get_client_factory(self): return JMClientProtocolFactory(self.taker)