def get_schedule(filename): with open(filename, "rb") as f: schedule = [] schedule_lines = f.readlines() for sl in schedule_lines: if sl.startswith("#"): continue try: mixdepth, amount, makercount, destaddr, waittime, completed = \ sl.split(',') except ValueError as e: return (False, "Failed to parse schedule line: " + sl) try: mixdepth = int(mixdepth) #TODO this isn't the right way, but floats must be allowed #for any persisted tumbler-style schedule if "." in amount: amount = float(amount) else: amount = int(amount) makercount = int(makercount) destaddr = destaddr.strip() waittime = float(waittime) completed = completed.strip() if not len(completed) == 64: completed = int(completed) except ValueError as e: return (False, "Failed to parse schedule line: " + sl) if destaddr not in ["INTERNAL", "addrask"]: success, errmsg = validate_address(destaddr) if not success: return (False, "Invalid address: " + destaddr + "," + errmsg) schedule.append([mixdepth, amount, makercount, destaddr, waittime, completed]) return (True, schedule)
def main(): parser = OptionParser( usage= 'usage: %prog [options] utxo destaddr1 destaddr2 ..', description="For creating multiple utxos from one (for commitments in JM)." "Provide a utxo in form txid:N that has some unspent coins;" "Specify a list of destination addresses and the coins will" "be split equally between them (after bitcoin fees)." "You'll be prompted to enter the private key for the utxo" "during the run; it must be in WIF compressed format." "After the transaction is completed, the utxo strings for" "the new outputs will be shown." "Note that these utxos will not be ready for use as external" "commitments in Joinmarket until 5 confirmations have passed." " BE CAREFUL about handling private keys!" " Don't do this in insecure environments." " Also note this ONLY works for standard (p2pkh) utxos." ) 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() if len(args) < 2: quit(parser, 'Invalid syntax') u = args[0] priv = raw_input( 'input private key for ' + u + ', in WIF compressed format : ') u, priv = get_utxo_info(','.join([u, priv])) if not u: quit(parser, "Failed to parse utxo info: " + u) destaddrs = args[1:] for d in destaddrs: if not validate_address(d): quit(parser, "Address was not valid; wrong network?: " + d) txsigned = sign(u, priv, destaddrs) log.debug("Got signed transaction:\n" + txsigned) log.debug("Deserialized:") log.debug(pformat(btc.deserialize(txsigned))) if raw_input('Would you like to push to the network? (y/n):')[0] != 'y': log.info("You chose not to broadcast the transaction, quitting.") return jm_single().bc_interface.pushtx(txsigned)
def main(): parser = OptionParser( usage='usage: %prog [options] utxo destaddr1 destaddr2 ..', description=description, formatter=IndentedHelpFormatterWithNL()) parser.add_option( '-t', '--utxo-address-type', action='store', dest='utxo_address_type', help= ('type of address of coin being spent - one of "p2pkh", "p2wpkh", "p2sh-p2wpkh". ' 'No other scriptpubkey types (e.g. multisig) are supported. If not set, we default ' 'to what is in joinmarket.cfg.'), default="") add_base_options(parser) (options, args) = parser.parse_args() load_program_config(config_path=options.datadir) if len(args) < 2: quit(parser, 'Invalid syntax') u = args[0] priv = input('input private key for ' + u + ', in WIF compressed format : ') u, priv = get_utxo_info(','.join([u, priv])) if not u: quit(parser, "Failed to parse utxo info: " + u) destaddrs = args[1:] for d in destaddrs: if not validate_address(d): quit(parser, "Address was not valid; wrong network?: " + d) success, utxo = utxostr_to_utxo(u) if not success: quit(parser, "Failed to load utxo from string: " + utxo) if options.utxo_address_type == "": 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" else: utxo_address_type = options.utxo_address_type txsigned = sign(utxo, priv, destaddrs, utxo_address_type) if not txsigned: log.info( "Transaction signing operation failed, see debug messages for details." ) return log.info("Got signed transaction:\n" + bintohex(txsigned.serialize())) log.info(btc.human_readable_transaction(txsigned)) if input('Would you like to push to the network? (y/n):')[0] != 'y': log.info("You chose not to broadcast the transaction, quitting.") return jm_single().bc_interface.pushtx(txsigned.serialize())
def validateSettings(self): valid, errmsg = validate_address(self.widgets[0][1].text()) if not valid: JMQtMessageBox(self, errmsg, mbtype='warn', title="Error") return False errs = ["Non-zero number of counterparties must be provided.", "Mixdepth must be chosen.", "Amount must be provided."] for i in [1, 3]: if self.widgets[i][1].text().size() == 0: JMQtMessageBox(self, errs[i - 1], mbtype='warn', title="Error") return False #QIntValidator does not prevent entry of 0 for counterparties. #Note, use of '1' is not recommended, but not prevented here. if self.widgets[1][1].text() == '0': JMQtMessageBox(self, errs[0], mbtype='warn', title="Error") return False #Note that a zero amount IS allowed (for sweep) cc = str(self.widgets[2][1].itemText(self.widgets[2][1].currentIndex())) self.choice_algo = self.c_choosers[cc] return True
def validateSettings(self): valid, errmsg = validate_address(self.widgets[0][1].text()) if not valid: JMQtMessageBox(self, errmsg, mbtype='warn', title="Error") return False errs = [ "Non-zero number of counterparties must be provided.", "Mixdepth must be chosen.", "Amount must be provided." ] for i in [1, 3]: if self.widgets[i][1].text().size() == 0: JMQtMessageBox(self, errs[i - 1], mbtype='warn', title="Error") return False #QIntValidator does not prevent entry of 0 for counterparties. #Note, use of '1' is not recommended, but not prevented here. if self.widgets[1][1].text() == '0': JMQtMessageBox(self, errs[0], mbtype='warn', title="Error") return False #Note that a zero amount IS allowed (for sweep) cc = str(self.widgets[2][1].itemText( self.widgets[2][1].currentIndex())) self.choice_algo = self.c_choosers[cc] return True
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)
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 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 = get_sendpayment_parser() (options, args) = parser.parse_args() load_program_config() if options.schedule == '' and len(args) < 3: parser.error('Needs a wallet, amount and destination address') sys.exit(0) #without schedule file option, use the arguments to create a schedule #of a single transaction sweeping = False if options.schedule == '': #note that sendpayment doesn't support fractional amounts, fractions throw #here. amount = int(args[1]) if amount == 0: sweeping = True destaddr = args[2] mixdepth = options.mixdepth addr_valid, errormsg = validate_address(destaddr) if not addr_valid: print('ERROR: Address invalid. ' + errormsg) return schedule = [[options.mixdepth, amount, options.makercount, destaddr, 0.0, 0]] else: result, schedule = get_schedule(options.schedule) if not result: log.info("Failed to load schedule file, quitting. Check the syntax.") log.info("Error was: " + str(schedule)) sys.exit(0) mixdepth = 0 for s in schedule: if s[1] == 0: sweeping = True #only used for checking the maximum mixdepth required mixdepth = max([mixdepth, s[0]]) wallet_name = args[0] #to allow testing of confirm/unconfirm callback for multiple txs if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface): jm_single().bc_interface.tick_forward_chain_interval = 10 jm_single().bc_interface.simulating = True jm_single().maker_timeout_sec = 15 chooseOrdersFunc = None if options.pickorders: chooseOrdersFunc = pick_order if sweeping: print('WARNING: You may have to pick offers multiple times') print('WARNING: due to manual offer picking while sweeping') elif options.choosecheapest: chooseOrdersFunc = cheapest_order_choose else: # choose randomly (weighted) chooseOrdersFunc = weighted_order_choose # Dynamically estimate a realistic fee if it currently is the default value. # At this point we do not know even the number of our own inputs, so # we guess conservatively with 2 inputs and 2 outputs each. if options.txfee == -1: options.txfee = max(options.txfee, estimate_tx_fee(2, 2, txtype="p2sh-p2wpkh")) log.debug("Estimated miner/tx fee for each cj participant: " + str( options.txfee)) assert (options.txfee >= 0) log.debug('starting sendpayment') if not options.userpcwallet: #maxmixdepth in the wallet is actually the *number* of mixdepths (so misnamed); #to ensure we have enough, must be at least (requested index+1) max_mix_depth = max([mixdepth+1, options.amtmixdepths]) wallet_path = get_wallet_path(wallet_name, None) wallet = open_test_wallet_maybe( wallet_path, wallet_name, max_mix_depth, gap_limit=options.gaplimit) else: raise NotImplemented("Using non-joinmarket wallet is not supported.") if jm_single().config.get("BLOCKCHAIN", "blockchain_source") == "electrum-server" and options.makercount != 0: jm_single().bc_interface.synctype = "with-script" #wallet sync will now only occur on reactor start if we're joining. while not jm_single().bc_interface.wallet_synced: sync_wallet(wallet, fast=options.fastsync) if options.makercount == 0: direct_send(wallet, amount, mixdepth, destaddr, options.answeryes) return if wallet.get_txtype() == 'p2pkh': print("Only direct sends (use -N 0) are supported for " "legacy (non-segwit) wallets.") return def filter_orders_callback(orders_fees, cjamount): orders, total_cj_fee = orders_fees log.info("Chose these orders: " +pprint.pformat(orders)) log.info('total cj fee = ' + str(total_cj_fee)) total_fee_pc = 1.0 * total_cj_fee / cjamount log.info('total coinjoin fee = ' + str(float('%.3g' % ( 100.0 * total_fee_pc))) + '%') WARNING_THRESHOLD = 0.02 # 2% if total_fee_pc > WARNING_THRESHOLD: log.info('\n'.join(['=' * 60] * 3)) log.info('WARNING ' * 6) log.info('\n'.join(['=' * 60] * 1)) log.info('OFFERED COINJOIN FEE IS UNUSUALLY HIGH. DOUBLE/TRIPLE CHECK.') log.info('\n'.join(['=' * 60] * 1)) log.info('WARNING ' * 6) log.info('\n'.join(['=' * 60] * 3)) if not options.answeryes: if raw_input('send with these orders? (y/n):')[0] != 'y': return False return True def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None): if fromtx == "unconfirmed": #If final entry, stop *here*, don't wait for confirmation if taker.schedule_index + 1 == len(taker.schedule): reactor.stop() return if fromtx: if res: txd, txid = txdetails taker.wallet.remove_old_utxos(txd) taker.wallet.add_new_utxos(txd, txid) reactor.callLater(waittime*60, clientfactory.getClient().clientStart) else: #a transaction failed; we'll try to repeat without the #troublemakers. #If this error condition is reached from Phase 1 processing, #and there are less than minimum_makers honest responses, we #just give up (note that in tumbler we tweak and retry, but #for sendpayment the user is "online" and so can manually #try again). #However if the error is in Phase 2 and we have minimum_makers #or more responses, we do try to restart with the honest set, here. if taker.latest_tx is None: #can only happen with < minimum_makers; see above. log.info("A transaction failed but there are insufficient " "honest respondants to continue; giving up.") reactor.stop() return #This is Phase 2; do we have enough to try again? taker.add_honest_makers(list(set( taker.maker_utxo_data.keys()).symmetric_difference( set(taker.nonrespondants)))) if len(taker.honest_makers) < jm_single().config.getint( "POLICY", "minimum_makers"): log.info("Too few makers responded honestly; " "giving up this attempt.") reactor.stop() return print("We failed to complete the transaction. The following " "makers responded honestly: ", taker.honest_makers, ", so we will retry with them.") #Now we have to set the specific group we want to use, and hopefully #they will respond again as they showed honesty last time. #we must reset the number of counterparties, as well as fix who they #are; this is because the number is used to e.g. calculate fees. #cleanest way is to reset the number in the schedule before restart. taker.schedule[taker.schedule_index][2] = len(taker.honest_makers) log.info("Retrying with: " + str(taker.schedule[ taker.schedule_index][2]) + " counterparties.") #rewind to try again (index is incremented in Taker.initialize()) taker.schedule_index -= 1 taker.set_honest_only(True) reactor.callLater(5.0, clientfactory.getClient().clientStart) else: if not res: log.info("Did not complete successfully, shutting down") #Should usually be unreachable, unless conf received out of order; #because we should stop on 'unconfirmed' for last (see above) else: log.info("All transactions completed correctly") reactor.stop() taker = Taker(wallet, schedule, order_chooser=chooseOrdersFunc, callbacks=(filter_orders_callback, None, taker_finished)) clientfactory = JMClientProtocolFactory(taker) nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False if jm_single().config.get("BLOCKCHAIN", "network") in ["regtest", "testnet"]: startLogging(sys.stdout) start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, daemon=daemon)
def main(): parser = get_sendpayment_parser() (options, args) = parser.parse_args() load_program_config() if options.schedule == '' and len(args) < 3: parser.error('Needs a wallet, amount and destination address') sys.exit(0) #without schedule file option, use the arguments to create a schedule #of a single transaction sweeping = False if options.schedule == '': #note that sendpayment doesn't support fractional amounts, fractions throw #here. amount = int(args[1]) if amount == 0: sweeping = True destaddr = args[2] mixdepth = options.mixdepth addr_valid, errormsg = validate_address(destaddr) if not addr_valid: print('ERROR: Address invalid. ' + errormsg) return schedule = [[ options.mixdepth, amount, options.makercount, destaddr, 0.0, 0 ]] else: result, schedule = get_schedule(options.schedule) if not result: log.info( "Failed to load schedule file, quitting. Check the syntax.") log.info("Error was: " + str(schedule)) sys.exit(0) mixdepth = 0 for s in schedule: if s[1] == 0: sweeping = True #only used for checking the maximum mixdepth required mixdepth = max([mixdepth, s[0]]) wallet_name = args[0] #to allow testing of confirm/unconfirm callback for multiple txs if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface): jm_single().bc_interface.tick_forward_chain_interval = 10 jm_single().bc_interface.simulating = True jm_single().maker_timeout_sec = 15 chooseOrdersFunc = None if options.pickorders: chooseOrdersFunc = pick_order if sweeping: print('WARNING: You may have to pick offers multiple times') print('WARNING: due to manual offer picking while sweeping') elif options.choosecheapest: chooseOrdersFunc = cheapest_order_choose else: # choose randomly (weighted) chooseOrdersFunc = weighted_order_choose # Dynamically estimate a realistic fee if it currently is the default value. # At this point we do not know even the number of our own inputs, so # we guess conservatively with 2 inputs and 2 outputs each. if options.txfee == -1: options.txfee = max(options.txfee, estimate_tx_fee(2, 2, txtype="p2sh-p2wpkh")) log.debug("Estimated miner/tx fee for each cj participant: " + str(options.txfee)) assert (options.txfee >= 0) log.debug('starting sendpayment') if not options.userpcwallet: #maxmixdepth in the wallet is actually the *number* of mixdepths (so misnamed); #to ensure we have enough, must be at least (requested index+1) max_mix_depth = max([mixdepth + 1, options.amtmixdepths]) if not os.path.exists(os.path.join('wallets', wallet_name)): wallet = get_wallet_cls()(wallet_name, None, max_mix_depth, options.gaplimit) else: while True: try: pwd = get_password("Enter wallet decryption passphrase: ") wallet = get_wallet_cls()(wallet_name, pwd, max_mix_depth, options.gaplimit) except WalletError: print("Wrong password, try again.") continue except Exception as e: print("Failed to load wallet, error message: " + repr(e)) sys.exit(0) break else: wallet = BitcoinCoreWallet(fromaccount=wallet_name) if jm_single().config.get( "BLOCKCHAIN", "blockchain_source" ) == "electrum-server" and options.makercount != 0: jm_single().bc_interface.synctype = "with-script" #wallet sync will now only occur on reactor start if we're joining. sync_wallet(wallet, fast=options.fastsync) if options.makercount == 0: if isinstance(wallet, BitcoinCoreWallet): raise NotImplementedError( "Direct send only supported for JM wallets") direct_send(wallet, amount, mixdepth, destaddr, options.answeryes) return def filter_orders_callback(orders_fees, cjamount): orders, total_cj_fee = orders_fees log.info("Chose these orders: " + pprint.pformat(orders)) log.info('total cj fee = ' + str(total_cj_fee)) total_fee_pc = 1.0 * total_cj_fee / cjamount log.info('total coinjoin fee = ' + str(float('%.3g' % (100.0 * total_fee_pc))) + '%') WARNING_THRESHOLD = 0.02 # 2% if total_fee_pc > WARNING_THRESHOLD: log.info('\n'.join(['=' * 60] * 3)) log.info('WARNING ' * 6) log.info('\n'.join(['=' * 60] * 1)) log.info( 'OFFERED COINJOIN FEE IS UNUSUALLY HIGH. DOUBLE/TRIPLE CHECK.') log.info('\n'.join(['=' * 60] * 1)) log.info('WARNING ' * 6) log.info('\n'.join(['=' * 60] * 3)) if not options.answeryes: if raw_input('send with these orders? (y/n):')[0] != 'y': return False return True def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None): if fromtx == "unconfirmed": #If final entry, stop *here*, don't wait for confirmation if taker.schedule_index + 1 == len(taker.schedule): reactor.stop() return if fromtx: if res: txd, txid = txdetails taker.wallet.remove_old_utxos(txd) taker.wallet.add_new_utxos(txd, txid) reactor.callLater(waittime * 60, clientfactory.getClient().clientStart) else: #a transaction failed; just stop reactor.stop() else: if not res: log.info("Did not complete successfully, shutting down") #Should usually be unreachable, unless conf received out of order; #because we should stop on 'unconfirmed' for last (see above) else: log.info("All transactions completed correctly") reactor.stop() taker = Taker(wallet, schedule, order_chooser=chooseOrdersFunc, callbacks=(filter_orders_callback, None, taker_finished)) clientfactory = JMClientProtocolFactory(taker) nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False if jm_single().config.get("BLOCKCHAIN", "network") in ["regtest", "testnet"]: startLogging(sys.stdout) start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, daemon=daemon)
def main_cs(test_data=None): #twisted logging (TODO disable for non-debug runs) if test_data: wallet_name, args, options, use_ssl, alt_class, alt_c_class, fail_alice_state, fail_carol_state = test_data server, port, usessl = parse_server_string(options.serverport) else: parser = get_coinswap_parser() (options, args) = parser.parse_args() #Will only be used by client server, port, usessl = parse_server_string(options.serverport) if options.checkonly: #no need for any more data; just query alice_client = CoinSwapJSONRPCClient(server[2:], port, usessl=usessl) reactor.callWhenRunning(alice_client.send_poll_unsigned, "status", print_status) reactor.run() return log.startLogging(sys.stdout) load_coinswap_config() wallet_name = args[0] #depth 0: spend in, depth 1: receive out, depth 2: for backout transactions. max_mix_depth = 3 wallet_dir = os.path.join(cs_single().homedir, 'wallets') if not os.path.exists(os.path.join(wallet_dir, wallet_name)): wallet = SegwitWallet(wallet_name, None, max_mix_depth, 6, wallet_dir=wallet_dir) else: while True: try: pwd = get_password("Enter wallet decryption passphrase: ") wallet = SegwitWallet(wallet_name, pwd, max_mix_depth, 6, wallet_dir=wallet_dir) 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 #for testing main script (not test framework), need funds. if not test_data and isinstance(cs_single().bc_interface, RegtestBitcoinCoreInterface): for i in range(3): cs_single().bc_interface.grab_coins( wallet.get_new_addr(0, 0, True), 2.0) wallet.index[0][0] -= 3 time.sleep(3) sync_wallet(wallet, fast=options.fastsync) if test_data: cs_single().bc_interface.wallet_synced = True wallet.used_coins = None if options.serve: #sanity check that client params were not provided: if len(args) > 1: print("Extra parameters provided for running as server. " "Are you sure you didn't want to run as client?") sys.exit(0) if not test_data: main_server(options, wallet) else: main_server( options, wallet, { 'use_ssl': use_ssl, 'alt_c_class': alt_c_class, 'fail_carol_state': fail_carol_state }) return wallet.get_balance_by_mixdepth() return if not options.recover: target_amount = int(args[1]) #Reset the targetting for backout transactions #TODO must be removed/changed for updated fees handling oldtarget = cs_single().config.get("POLICY", "tx_fees") newtarget = cs_single().config.getint("POLICY", "backout_fee_target") multiplier = float(cs_single().config.get("POLICY", "backout_fee_multiplier")) cs_single().config.set("POLICY", "tx_fees", str(newtarget)) tx23fee = estimate_tx_fee((1, 2, 2), 1, txtype='p2shMofN') tx23fee = int(multiplier * tx23fee) tx24_recipient_amount = target_amount - tx23fee tx35_recipient_amount = target_amount - tx23fee cs_single().config.set("POLICY", "tx_fees", oldtarget) #to allow testing of confirm/unconfirm callback for multiple txs if isinstance(cs_single().bc_interface, RegtestBitcoinCoreInterface): cs_single().bc_interface.tick_forward_chain_interval = 2 cs_single().bc_interface.simulating = True cs_single().config.set("BLOCKCHAIN", "notify_port", "62652") cs_single().config.set("BLOCKCHAIN", "rpc_host", "127.0.0.2") #if restart option selected, read state and backout if options.recover: session_id = options.recover alice = CoinSwapAlice(wallet, 'alicestate') alice.bbmb = wallet.get_balance_by_mixdepth(verbose=False) alice.load(sessionid=session_id) alice.backout("Recovering from shutdown") reactor.run() return if len(args) > 2: tx5address = args[2] if not validate_address(tx5address): print("Invalid address: ", tx5address) sys.exit(0) else: #Our destination address should be in a separate mixdepth tx5address = wallet.get_new_addr(1, 1, True) #instantiate the parameters, but don't yet have the ephemeral pubkeys #or destination addresses. #TODO figure out best estimate incl. priority btcfee_est = estimate_tx_fee((1, 2, 2), 1, txtype='p2shMofN') cpp = CoinSwapPublicParameters(base_amount=target_amount, bitcoin_fee=btcfee_est) cpp.set_addr_data(addr5=tx5address) testing_mode = True if test_data else False aliceclass = alt_class if test_data and alt_class else CoinSwapAlice if test_data and fail_alice_state: alice = aliceclass(wallet, 'alicestate', cpp, testing_mode=testing_mode, fail_state=fail_alice_state) else: if testing_mode or options.checkfee: alice = aliceclass(wallet, 'alicestate', cpp, testing_mode=testing_mode) else: alice = aliceclass(wallet, 'alicestate', cpp, testing_mode=testing_mode, fee_checker="cli") alice_client = CoinSwapJSONRPCClient(server[2:], port, alice.sm.tick, alice.backout, usessl) alice.set_jsonrpc_client(alice_client) reactor.callWhenRunning(alice_client.send_poll_unsigned, "status", alice.check_server_status) if not test_data: reactor.run() if test_data: return alice
def main_cs(test_data=None): #twisted logging (TODO disable for non-debug runs) if test_data: wallet_name, args, options, use_ssl, alt_class, alt_c_class = test_data else: log.startLogging(sys.stdout) #Joinmarket wallet parser = get_coinswap_parser() (options, args) = parser.parse_args() load_coinswap_config() wallet_name = args[0] #depth 0: spend in, depth 1: receive out, depth 2: for backout transactions. max_mix_depth = 3 if not os.path.exists(os.path.join('wallets', wallet_name)): wallet = Wallet(wallet_name, None, max_mix_depth, 6) else: while True: try: pwd = get_password("Enter wallet decryption passphrase: ") wallet = Wallet(wallet_name, pwd, max_mix_depth, 6) 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 #for testing main script (not test framework), need funds. if not test_data and isinstance(cs_single().bc_interface, RegtestBitcoinCoreInterface): cs_single().bc_interface.grab_coins(wallet.get_new_addr(0, 0), 2.0) time.sleep(3) sync_wallet(wallet, fast=options.fastsync) wallet.used_coins = None if options.serve: #sanity check that client params were not provided: if len(args) > 1: print("Extra parameters provided for running as server. " "Are you sure you didn't want to run as client?") sys.exit(0) if not test_data: main_server(options, wallet) else: main_server(options, wallet, { 'use_ssl': use_ssl, 'alt_c_class': alt_c_class }) return wallet.get_balance_by_mixdepth() return tx01_amount = int(args[1]) #Reset the targetting for backout transactions oldtarget = cs_single().config.get("POLICY", "tx_fees") newtarget = cs_single().config.getint("POLICY", "backout_fee_target") multiplier = float(cs_single().config.get("POLICY", "backout_fee_multiplier")) cs_single().config.set("POLICY", "tx_fees", str(newtarget)) tx23fee = estimate_tx_fee((1, 2, 2), 1, txtype='p2shMofN') tx23fee = int(multiplier * tx23fee) tx24_recipient_amount = tx01_amount - tx23fee tx35_recipient_amount = tx01_amount - tx23fee cs_single().config.set("POLICY", "tx_fees", oldtarget) #to allow testing of confirm/unconfirm callback for multiple txs if isinstance(cs_single().bc_interface, RegtestBitcoinCoreInterface): cs_single().bc_interface.tick_forward_chain_interval = 2 cs_single().bc_interface.simulating = True cs_single().config.set("BLOCKCHAIN", "notify_port", "62652") cs_single().config.set("BLOCKCHAIN", "rpc_host", "127.0.0.2") #if restart option selected, read state and backout if options.recover: session_id = options.recover alice = CoinSwapAlice(wallet, 'alicestate') alice.bbmb = wallet.get_balance_by_mixdepth(verbose=False) alice.load(sessionid=session_id) alice.backout("Recovering from shutdown") reactor.run() return if len(args) > 2: tx5address = args[2] if not validate_address(tx5address): print("Invalid address: ", tx5address) sys.exit(0) else: #Our destination address should be in a separate mixdepth tx5address = wallet.get_new_addr(1, 1) #instantiate the parameters, but don't yet have the ephemeral pubkeys #or destination addresses. cpp = CoinSwapPublicParameters(tx01_amount, tx24_recipient_amount, tx35_recipient_amount) #Alice must set the unique identifier for this run. cpp.set_session_id() cpp.set_tx5_address(tx5address) testing_mode = True if test_data else False aliceclass = alt_class if test_data and alt_class else CoinSwapAlice alice = aliceclass(wallet, 'alicestate', cpp, testing_mode=testing_mode) scheme, server, port = options.serverport.split(":") print("got this scheme, server, port: ", scheme, server, port) if scheme == "https": usessl = True elif scheme == "http": usessl = False else: print("Invalid server string: ", options.serverport) sys.exit(0) if not server[:2] == "//": print("Invalid server string: ", options.serverport) alice_client = CoinSwapJSONRPCClient(server[2:], port, alice.sm.tick, alice.backout, usessl) alice.set_jsonrpc_client(alice_client) reactor.callWhenRunning(alice.sm.tick) if not test_data: reactor.run() if test_data: return alice
segwit=True) print( "Created the following one-in, one-out transaction, which will not " "be valid to broadcast itself (negative fee). Pass it to your " "counterparty:") print(pformat(btc.deserialize(serialized_single_acp))) print("Pass the following raw hex to your counterparty:") print(serialized_single_acp) exit(0) elif args[1] == "take": try: amount, destaddr, txhex = args[2:5] #sanity check input amount = int(amount) assert amount > 0 assert validate_address(destaddr) binascii.unhexlify(txhex) except Exception as e: print("Syntax error, should be 5 arguments, see --help. ", repr(e)) exit(0) success, complete_tx = graft_onto_single_acp(wallet, txhex, amount, destaddr) if not success: print("Quitting, reason: " + complete_tx) exit(0) #allow user to decide whether to broadcast: print("The following transaction has been prepared:") print(pformat(btc.deserialize(complete_tx))) broadcast = raw_input("Do you want to broadcast now? (y/n): ") if broadcast == "y": success = jm_single().bc_interface.pushtx(complete_tx)
def main(): parser = OptionParser( usage= 'usage: %prog [options] utxo destaddr1 destaddr2 ..', description="For creating multiple utxos from one (for commitments in JM)." "Provide a utxo in form txid:N that has some unspent coins;" "Specify a list of destination addresses and the coins will" "be split equally between them (after bitcoin fees)." "You'll be prompted to enter the private key for the utxo" "during the run; it must be in WIF compressed format." "After the transaction is completed, the utxo strings for" "the new outputs will be shown." "Note that these utxos will not be ready for use as external" "commitments in Joinmarket until 5 confirmations have passed." " BE CAREFUL about handling private keys!" " Don't do this in insecure environments." " Works only with p2pkh ('1') or p2sh-p2wpkh (segwit '3')" " utxos - set segwit=False in the POLICY section of" " joinmarket.cfg for the former." ) parser.add_option( '-t', '--utxo-address-type', action='store', dest='utxo_address_type', help=('type of address of coin being spent - one of "p2pkh", "p2wpkh", "p2sh-p2wpkh". ' 'No other scriptpubkey types (e.g. multisig) are supported. If not set, we default ' 'to what is in joinmarket.cfg.'), default="" ) add_base_options(parser) (options, args) = parser.parse_args() load_program_config(config_path=options.datadir) if len(args) < 2: quit(parser, 'Invalid syntax') u = args[0] priv = input( 'input private key for ' + u + ', in WIF compressed format : ') u, priv = get_utxo_info(','.join([u, priv])) if not u: quit(parser, "Failed to parse utxo info: " + u) destaddrs = args[1:] for d in destaddrs: if not validate_address(d): quit(parser, "Address was not valid; wrong network?: " + d) success, utxo = utxostr_to_utxo(u) if not success: quit(parser, "Failed to load utxo from string: " + utxo) if options.utxo_address_type == "": 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" else: utxo_address_type = options.utxo_address_type txsigned = sign(utxo, priv, destaddrs, utxo_address_type) if not txsigned: log.info("Transaction signing operation failed, see debug messages for details.") return log.info("Got signed transaction:\n" + bintohex(txsigned.serialize())) log.info(btc.human_readable_transaction(txsigned)) if input('Would you like to push to the network? (y/n):')[0] != 'y': log.info("You chose not to broadcast the transaction, quitting.") return jm_single().bc_interface.pushtx(txsigned.serialize())
def main(): parser = OptionParser( usage='usage: %prog [options] utxo destaddr1 destaddr2 ..', description= "For creating multiple utxos from one (for commitments in JM)." "Provide a utxo in form txid:N that has some unspent coins;" "Specify a list of destination addresses and the coins will" "be split equally between them (after bitcoin fees)." "You'll be prompted to enter the private key for the utxo" "during the run; it must be in WIF compressed format." "After the transaction is completed, the utxo strings for" "the new outputs will be shown." "Note that these utxos will not be ready for use as external" "commitments in Joinmarket until 5 confirmations have passed." " BE CAREFUL about handling private keys!" " Don't do this in insecure environments." " Works only with p2pkh ('1') or p2sh-p2wpkh (segwit '3')" " utxos - set segwit=False in the POLICY section of" " joinmarket.cfg for the former.") 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) parser.add_option( '-n', '--non-segwit-input', action='store_true', dest='nonsegwit', help= 'input is p2pkh ("1" address), not segwit; if not used, input is assumed to be segwit type.', default=False) add_base_options(parser) (options, args) = parser.parse_args() load_program_config(config_path=options.datadir) if len(args) < 2: quit(parser, 'Invalid syntax') u = args[0] priv = input('input private key for ' + u + ', in WIF compressed format : ') u, priv = get_utxo_info(','.join([u, priv])) if not u: quit(parser, "Failed to parse utxo info: " + u) destaddrs = args[1:] for d in destaddrs: if not validate_address(d): quit(parser, "Address was not valid; wrong network?: " + d) success, utxo = utxostr_to_utxo(u) if not success: quit(parser, "Failed to load utxo from string: " + utxo) txsigned = sign(utxo, priv, destaddrs, segwit=not options.nonsegwit) if not txsigned: log.info( "Transaction signing operation failed, see debug messages for details." ) return log.info("Got signed transaction:\n" + bintohex(txsigned.serialize())) log.debug("Deserialized:") log.debug(pformat(str(txsigned))) if input('Would you like to push to the network? (y/n):')[0] != 'y': log.info("You chose not to broadcast the transaction, quitting.") return jm_single().bc_interface.pushtx(txsigned.serialize())
def process_new_addr_response(self, response, code): assert code == 200 json_body = json.loads(response.decode("utf-8")) assert validate_address(json_body["address"])[0]