def receive_payjoin_main(): parser = OptionParser(usage='usage: %prog [options] [wallet file] [amount-to-receive]') add_base_options(parser) parser.add_option('-P', '--hs-port', action='store', type='int', dest='hsport', default=80, help='port on which to serve the ephemeral hidden service.') parser.add_option('-g', '--gap-limit', action='store', type="int", dest='gaplimit', default=6, help='gap limit for wallet, default=6') parser.add_option('-m', '--mixdepth', action='store', type='int', dest='mixdepth', default=0, help="mixdepth to source coins from") parser.add_option('-a', '--amtmixdepths', action='store', type='int', dest='amtmixdepths', help='number of mixdepths in wallet, default 5', default=5) (options, args) = parser.parse_args() if len(args) < 2: parser.error('Needs a wallet, and a receiving amount in bitcoins or satoshis') sys.exit(EXIT_ARGERROR) wallet_name = args[0] try: # amount is stored internally in sats, but will be decimal in URL. bip78_amount = amount_to_sat(args[1]) except: parser.error("Invalid receiving amount passed: " + bip78_amount) sys.exit(EXIT_FAILURE) if bip78_amount < 0: parser.error("Receiving amount must be a positive number") sys.exit(EXIT_FAILURE) load_program_config(config_path=options.datadir) check_regtest() wallet_path = get_wallet_path(wallet_name, None) max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1]) wallet = open_test_wallet_maybe( wallet_path, wallet_name, max_mix_depth, wallet_password_stdin=options.wallet_password_stdin, gap_limit=options.gaplimit) wallet_service = WalletService(wallet) while not wallet_service.synced: wallet_service.sync_wallet(fast=not options.recoversync) wallet_service.startService() # having enforced wallet sync, we can check if we have coins # to do payjoin in the mixdepth if wallet_service.get_balance_by_mixdepth()[options.mixdepth] == 0: jlog.error("Cannot do payjoin from mixdepth " + str( options.mixdepth) + ", no coins. Shutting down.") sys.exit(EXIT_ARGERROR) receiver_manager = JMBIP78ReceiverManager(wallet_service, options.mixdepth, bip78_amount, options.hsport) receiver_manager.start_pj_server_and_tor() reactor.run()
def do_test_payment(self, wc1, wc2, amt=1.1): wallet_structures = [self.wallet_structure] * 2 wallet_cls = (wc1, wc2) self.wallet_services = [] self.wallet_services.append(make_wallets_to_list(make_wallets( 1, wallet_structures=[wallet_structures[0]], mean_amt=self.mean_amt, wallet_cls=wallet_cls[0]))[0]) self.wallet_services.append(make_wallets_to_list(make_wallets( 1, wallet_structures=[wallet_structures[1]], mean_amt=self.mean_amt, wallet_cls=wallet_cls[1]))[0]) jm_single().bc_interface.tickchain() sync_wallets(self.wallet_services) # For accounting purposes, record the balances # at the start. self.rsb = getbals(self.wallet_services[0], 0) self.ssb = getbals(self.wallet_services[1], 0) self.cj_amount = int(amt * 10**8) def cbStopListening(): return self.port.stopListening() b78rm = JMBIP78ReceiverManager(self.wallet_services[0], 0, self.cj_amount, 47083) resource = DummyBIP78ReceiverResource(jmprint, cbStopListening, b78rm) self.site = Site(resource) self.site.displayTracebacks = False # NB The connectivity aspects of the onion-based BIP78 setup # are time heavy. This server is TCP only. self.port = reactor.listenTCP(47083, self.site) self.addCleanup(cbStopListening) # setup of spender bip78_btc_amount = amount_to_btc(amount_to_sat(self.cj_amount)) bip78_uri = encode_bip21_uri(str(b78rm.receiving_address), {"amount": bip78_btc_amount, "pj": b"http://127.0.0.1:47083"}, safe=":/") self.manager = parse_payjoin_setup(bip78_uri, self.wallet_services[1], 0) self.manager.mode = "testing" success, msg = make_payment_psbt(self.manager) assert success, msg params = make_payjoin_request_params(self.manager) # avoiding backend daemon (testing only jmclient code here), # we send the http request manually: serv = b"http://127.0.0.1:47083" agent = get_nontor_agent() body = BytesProducer(self.manager.initial_psbt.to_base64().encode("utf-8")) url_parts = list(wrapped_urlparse(serv)) url_parts[4] = urlencode(params).encode("utf-8") destination_url = urlparse.urlunparse(url_parts) d = agent.request(b"POST", destination_url, Headers({"Content-Type": ["text/plain"]}), bodyProducer=body) d.addCallback(bip78_receiver_response, self.manager) return d
def decode_bip21_uri(uri): if not is_bip21_uri(uri): raise ValueError("Not a valid BIP21 URI: " + uri) result = {} parsed = urlparse(uri) result['address'] = parsed.path params = parse_qs(parsed.query) for key in params: if key.startswith('req-'): raise ValueError("Unknown required parameter " + key + " in BIP21 URI.") if key == 'amount': amount_str = params['amount'][0] if not is_bip21_amount_str(amount_str): raise ValueError("Invalid BTC amount " + amount_str + " vs. " + str(amount_to_sat(amount_str)) + " sat") # Convert amount to sats, as used internally by JM result['amount'] = amount_to_sat(amount_str + "btc") else: result[key] = urldecode(params[key][0]) return result
def bip21_uri_from_onion_hostname(self, host): """ Encoding the BIP21 URI according to BIP78 specifications, and specifically only supporting a hidden service endpoint. Note: we hardcode http; no support for TLS over HS. Second, note we convert the amount-in-sats self.amount to BTC denomination as expected by BIP21. """ port_str = ":" + str(self.port) if self.port != 80 else "" full_pj_string = "http://" + host + port_str bip78_btc_amount = btc.amount_to_btc(btc.amount_to_sat(self.amount)) # "safe" option is required to encode url in url unmolested: bip21_uri = btc.encode_bip21_uri(str(self.receiving_address), { "amount": bip78_btc_amount, "pj": full_pj_string.encode("utf-8") }, safe=":/") self.info_callback("Your hidden service is available. Please\n" "now pass this URI string to the sender to\n" "effect the payjoin payment:") self.uri_created_callback(bip21_uri) if self.mode == "command-line": self.info_callback("Keep this process running until the payment " "is received.")
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 receive_payjoin_main(makerclass): parser = OptionParser( usage='usage: %prog [options] [wallet file] [amount-to-receive]') parser.add_option('-g', '--gap-limit', action='store', type="int", dest='gaplimit', default=6, help='gap limit for wallet, default=6') parser.add_option('--recoversync', action='store_true', dest='recoversync', default=False, help=('choose to do detailed wallet sync, ' 'used for recovering on new Core instance.')) parser.add_option('-m', '--mixdepth', action='store', type='int', dest='mixdepth', default=0, help="mixdepth to source coins from") parser.add_option('-a', '--amtmixdepths', action='store', type='int', dest='amtmixdepths', help='number of mixdepths in wallet, default 5', default=5) parser.add_option('--wallet-password-stdin', action='store_true', default=False, dest='wallet_password_stdin', help='Read wallet password from stdin') (options, args) = parser.parse_args() if len(args) < 2: parser.error( 'Needs a wallet, and a receiving amount in bitcoins or satoshis') sys.exit(EXIT_ARGERROR) wallet_name = args[0] try: receiving_amount = amount_to_sat(args[1]) except: parser.error("Invalid receiving amount passed: " + receiving_amount) sys.exit(EXIT_FAILURE) if receiving_amount < 0: parser.error("Receiving amount must be a positive number") sys.exit(EXIT_FAILURE) load_program_config() check_regtest() # This workflow requires command line reading; we force info level logging # to remove noise, and mostly communicate to the user with the fn # log.info (via P2EPMaker.user_info). set_logging_level("INFO") wallet_path = get_wallet_path(wallet_name, 'wallets') max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1]) wallet = open_test_wallet_maybe( wallet_path, wallet_name, max_mix_depth, wallet_password_stdin=options.wallet_password_stdin, gap_limit=options.gaplimit) wallet_service = WalletService(wallet) while not wallet_service.synced: wallet_service.sync_wallet(fast=not options.recoversync) wallet_service.startService() maker = makerclass(wallet_service, options.mixdepth, receiving_amount) jlog.info('starting receive-payjoin') clientfactory = JMClientProtocolFactory(maker, proto_type="MAKER") nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False if jm_single().config.get("BLOCKCHAIN", "network") in ["regtest", "testnet"]: startLogging(sys.stdout) start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), clientfactory, daemon=daemon, p2ep=True)
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("PayJoin requires exactly three arguments: " "wallet, amount and destination address.") sys.exit(EXIT_ARGERROR) elif options.schedule == '' and len(args) != 3: parser.error("Joinmarket sendpayment (coinjoin) needs arguments:" " wallet, amount and destination address") sys.exit(EXIT_ARGERROR) #without schedule file option, use the arguments to create a schedule #of a single transaction sweeping = False if options.schedule == '': amount = btc.amount_to_sat(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") 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 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 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%}, {} " "".format(maxcjfee[0], btc.amount_to_str(maxcjfee[1]))) 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, 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) * 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_service, 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") 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)) 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_amount_to_sat(): assert (btc.amount_to_sat("1") == 1) assert (btc.amount_to_sat("1sat") == 1) assert (btc.amount_to_sat("1.123sat") == 1) assert (btc.amount_to_sat("0.00000001") == 1) assert (btc.amount_to_sat("0.00000001btc") == 1) assert (btc.amount_to_sat("0.00000001BTC") == 1) assert (btc.amount_to_sat("1.00000000") == 100000000) assert (btc.amount_to_sat("1.12300000sat") == 1) assert (btc.amount_to_sat("1btc") == 100000000) assert (btc.amount_to_sat("1BTC") == 100000000) with pytest.raises(ValueError): btc.amount_to_sat("") btc.amount_to_sat("invalidamount") btc.amount_to_sat("123inv")
def main() -> None: parser = OptionParser( usage="usage: %prog [options] UTXO or amount", description=DESCRIPTION, ) add_base_options(parser) parser.add_option( "-i", "--interest", action="store", type="float", dest="interest", help= "Interest rate to use for fidelity bond calculation (instead of interest_rate config)", ) parser.add_option( "-e", "--exponent", action="store", type="float", dest="exponent", help= "Exponent to use for fidelity bond calculation (instead of bond_value_exponent config)", ) parser.add_option( "-m", "--months", action="store", type="int", dest="months", help= "For how many months to calculate the fidelity bond values, each month has its own stats (default 12)", default=12, ) parser.add_option( "-o", "--orderbook", action="store", type="str", dest="path_to_json", help="Path to the exported orderbook in JSON format", ) options, args = parser.parse_args() load_program_config(config_path=options.datadir) if len(args) != 1: log.error("Invalid arguments, see --help") sys.exit(EXIT_ARGERROR) if options.path_to_json: try: with open(options.path_to_json, "r", encoding="UTF-8") as orderbook: orderbook = loads(orderbook.read()) except FileNotFoundError as exc: log.error(exc) sys.exit(EXIT_ARGERROR) else: orderbook = None try: amount = amount_to_sat(args[0]) confirm_time = None except ValueError: # If it's not a valid amount then it has to be a UTXO if jm_single().bc_interface is None: log.error( "For calculation based on UTXO access to Bitcoin Core is required" ) sys.exit(EXIT_FAILURE) success, utxo = utxostr_to_utxo(args[0]) if not success: # utxo contains the error message log.error(utxo) sys.exit(EXIT_ARGERROR) utxo_data = jm_single().bc_interface.query_utxo_set( utxo, includeconfs=True)[0] amount = utxo_data["value"] if utxo_data["confirms"] == 0: log.warning( "Given UTXO is unconfirmed, current time will be used as confirmation time" ) confirm_time = None elif utxo_data["confirms"] < 0: log.error("Given UTXO is invalid, reason: conflicted") sys.exit(EXIT_ARGERROR) else: current_height = jm_single().bc_interface.get_current_block_height( ) block_hash = jm_single().bc_interface.get_block_hash( current_height - utxo_data["confirms"] + 1) confirm_time = jm_single().bc_interface.get_block_time(block_hash) parameters, results = get_bond_values(amount, options.months, confirm_time, options.interest, options.exponent, orderbook) jmprint(f"Amount locked: {amount} ({sat_to_btc(amount)} btc)") jmprint( f"Confirmation time: {datetime.fromtimestamp(parameters['confirm_time'])}" ) jmprint( f"Interest rate: {parameters['interest']} ({parameters['interest'] * 100}%)" ) jmprint(f"Exponent: {parameters['exponent']}") jmprint(f"\nFIDELITY BOND VALUES (BTC^{parameters['exponent']})") jmprint("\nSee /docs/fidelity-bonds.md for complete formula and more") for result in results: locktime = datetime.fromtimestamp(result["locktime"]) # Mimic the locktime value the user would have to insert to create such fidelity bond jmprint(f"\nLocktime: {locktime.year}-{locktime.month}") # Mimic orderbook value jmprint( f"Bond value: {float(Decimal(result['value']) / Decimal(1e16)):.16f}" ) if options.path_to_json: jmprint( f"Weight: {result['weight']:.5f} ({result['weight'] * 100:.2f}% of all bonds)" ) jmprint(f"Top {result['percentile']}% of the orderbook by value")