def load_wallet(self, wallet, window): """The main entry point for the joinmarket plugin; create the joinmarket tab and initialize the joinmarket_core code. """ #can get called via direct hook or on_new_window #(the latter happens if we just enabled it in the plugins menu). #Insist on loading only once. if self.started: return #refuse to load the plugin for non-standard wallets. if wallet.wallet_type != "standard": return if not self.config_location: self.load_config(window) #set the access to the network for the custom #dummy blockchain interface (reads blockchain via wallet.network) jm_single().bc_interface.set_wallet(wallet) self.wallet = wallet #currently only coinjoins with all-p2pkh are supported in JM self.wallet.txin_type = 'p2pkh' self.window = window self.wrap_wallet = ElectrumWrapWallet(self.wallet) self.jmtab = JoinmarketTab(self) #needed to receive notification of unconfirmed transactions: self.wallet.network.register_callback(self.jmtab.on_new_tx, ['new_transaction']) self.window.tabs.addTab(self.jmtab, _('Joinmarket')) self.started = True
def handleEdit(self, section, t, checked=None): if isinstance(t[1], QCheckBox): oname = str(t[0].text()) oval = 'true' if checked else 'false' log.debug('setting section: ' + section + ' and name: ' + oname + ' to: ' + oval) jm_single().config.set(section, oname, oval) else: #currently there is only QLineEdit log.debug('setting section: ' + section + ' and name: ' + str(t[ 0].text()) + ' to: ' + str(t[1].text())) jm_single().config.set(section, str(t[0].text()), str(t[1].text()))
def showAboutDialog(self): msgbox = QDialog(self) lyt = QVBoxLayout(msgbox) msgbox.setWindowTitle("About the joinmarket electrum plugin") label1 = QLabel() label1.setText( "<a href=" + "'https://github.com/AdamISZ/electrum-joinmarket-plugin'>" + "Read more about this plugin.</a><p>" + "<p>".join( ["Joinmarket electrum plugin version: " + PLUGIN_VERSION, "Messaging protocol version:" + " %s" % ( str(jm_single().JM_VERSION) ), "Support this plugin -", "donate here: "])) label2 = QLabel(donation_address) for l in [label1, label2]: l.setTextFormat(QtCore.Qt.RichText) l.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) l.setOpenExternalLinks(True) label2.setText("<a href='bitcoin:" + donation_address + "'>" + donation_address + "</a>") lyt.addWidget(label1) lyt.addWidget(label2) btnbox = QDialogButtonBox(msgbox) btnbox.setStandardButtons(QDialogButtonBox.Ok) btnbox.accepted.connect(msgbox.accept) lyt.addWidget(btnbox) msgbox.exec_()
def initUI(self): outerGrid = QGridLayout() sA = QScrollArea() sA.setWidgetResizable(True) frame = QFrame() grid = QGridLayout() self.settingsFields = [] j = 0 #Simplified from Joinmarket-Qt: #many internal settings are not relevant for Electrum #However, additional settings from POLICY which must be exposed: #txfees, absurd_fees_per_kb and minimum_makers sections = ["POLICY", "MESSAGING", "GUI"] for section in sections: pairs = jm_single().config.items(section) newSettingsFields = self.getSettingsFields(section, [_[0] for _ in pairs]) self.settingsFields.extend(newSettingsFields) sL = QLabel(section) sL.setStyleSheet("QLabel {color: blue;}") grid.addWidget(sL) j += 1 for k, ns in enumerate(newSettingsFields): grid.addWidget(ns[0], j, 0) #try to find the tooltip for this label from config tips; #it might not be there if str(ns[0].text()) in config_tips: ttS = config_tips[str(ns[0].text())] ns[0].setToolTip(ttS) grid.addWidget(ns[1], j, 1) sfindex = len(self.settingsFields) - len(newSettingsFields) + k if isinstance(ns[1], QCheckBox): ns[1].toggled.connect(lambda checked, s=section, q=sfindex: self.handleEdit( s, self.settingsFields[q], checked)) else: ns[1].editingFinished.connect( lambda q=sfindex, s=section: self.handleEdit(s, self.settingsFields[q])) j += 1 outerGrid.addWidget(sA) ok_button = QPushButton("OK") ok_button.clicked.connect(self.close) outerGrid.addWidget(ok_button) sA.setWidget(frame) frame.setLayout(grid) frame.adjustSize() self.setLayout(outerGrid) self.setModal(True) self.show()
def load_config(self, window): """Load/instantiate the joinmarket config file in electrum's home directory/joinmarket (e.g. ~/.electrum/joinmarket Also load/instantiate the logs/ subdirectory for bot logs, and the cmtdata/ directory for commitments storage. Create and set the commitments.json file. """ try: jm_subdir = os.path.join(window.config.path, "joinmarket") if not os.path.exists(jm_subdir): os.makedirs(jm_subdir) cmttools_dir = os.path.join(jm_subdir, "cmtdata") if not os.path.exists(cmttools_dir): os.makedirs(cmttools_dir) set_commitment_file(os.path.join(cmttools_dir, "commitments.json")) self.config_location = os.path.join(jm_subdir, "joinmarket.cfg") self.logs_location = os.path.join(jm_subdir, "logs") load_program_config(jm_subdir, "electrum") if TESTNET: jm_single().config.set("BLOCKCHAIN", "network", "testnet") log.info('working with testnet') else: jm_single().config.set("BLOCKCHAIN", "network", "mainnet") log.info('working with mainnet') except Exception as e: log.info("thrown: " + repr(e)) JMQtMessageBox(window, "\n".join([ "The joinmarket config failed to load.", "Make sure that blockchain_source = electrum", "is set in the joinmarket.cfg file."]), mbtype='warn', title="Error") return if not os.path.exists(self.logs_location): os.makedirs(self.logs_location) update_config_for_gui()
def update_config_for_gui(): '''The default joinmarket config does not contain these GUI settings (they are generally set by command line flags or not needed). If they are set in the file, use them, else set the defaults. These *will* be persisted to joinmarket.cfg, but that will not affect operation of the command line version. ''' gui_config_names = ['check_high_fee', 'order_wait_time', 'daemon_port'] gui_config_default_vals = ['2', '30', '27183'] if "GUI" not in jm_single().config.sections(): jm_single().config.add_section("GUI") gui_items = jm_single().config.items("GUI") for gcn, gcv in zip(gui_config_names, gui_config_default_vals): if gcn not in [_[0] for _ in gui_items]: jm_single().config.set("GUI", gcn, gcv)
def getSettingsFields(self, section, names): results = [] for name in names: if section == "POLICY" and name not in config_types: continue val = jm_single().config.get(section, name) if name in config_types: t = config_types[name] if t == bool: qt = QCheckBox() if val.lower() == 'true': qt.setChecked(True) elif not t: continue else: qt = QLineEdit(val) if t == int: qt.setValidator(QIntValidator(0, 65535)) else: qt = QLineEdit(val) results.append((QLabel(name), qt)) return results
def test_taker_init(createcmtdata, schedule, highfee, toomuchcoins, minmakers, notauthed, ignored, nocommit): #these tests do not trigger utxo_retries oldtakerutxoretries = jm_single().config.get("POLICY", "taker_utxo_retries") oldtakerutxoamtpercent = jm_single().config.get("POLICY", "taker_utxo_amtpercent") jm_single().config.set("POLICY", "taker_utxo_retries", "20") def clean_up(): jm_single().config.set("POLICY", "minimum_makers", oldminmakers) jm_single().config.set("POLICY", "taker_utxo_retries", oldtakerutxoretries) jm_single().config.set("POLICY", "taker_utxo_amtpercent", oldtakerutxoamtpercent) oldminmakers = jm_single().config.get("POLICY", "minimum_makers") jm_single().config.set("POLICY", "minimum_makers", str(minmakers)) taker = get_taker(schedule) orderbook = copy.deepcopy(t_orderbook) if highfee: for o in orderbook: #trigger high-fee warning; but reset in next step o['cjfee'] = '1.0' if ignored: taker.ignored_makers = ignored if nocommit: jm_single().config.set("POLICY", "taker_utxo_amtpercent", nocommit) if schedule[0][1] == 0.2: #triggers calc-ing amount based on a fraction jm_single().mincjamount = 50000000 #bigger than 40m = 0.2 * 200m res = taker.initialize(orderbook) assert res[0] assert res[1] == jm_single().mincjamount return clean_up() res = taker.initialize(orderbook) if toomuchcoins or ignored: assert not res[0] return clean_up() if nocommit: print(str(res)) assert not res[0] return clean_up() taker.orderbook = copy.deepcopy( t_chosen_orders) #total_cjfee unaffected, all same maker_response = copy.deepcopy(t_maker_response) if notauthed: #Doctor one of the maker response data fields maker_response["J659UPUSLLjHJpaB"][1] = "xx" #the auth pub if schedule[0][1] == 199850000: #triggers negative change #makers offer 3000 txfee; we estimate ~ 147*10 + 2*34 + 10=1548 bytes #times 30k = 46440, so we pay 43440, plus maker fees = 3*0.0002*200000000 #roughly, gives required selected = amt + 163k, hence the above = #2btc - 150k sats = 199850000 (tweaked because of aggressive coin selection) #simulate the effect of a maker giving us a lot more utxos taker.utxos["dummy_for_negative_change"] = ["a", "b", "c", "d", "e"] with pytest.raises(ValueError) as e_info: res = taker.receive_utxos(maker_response) return clean_up() if schedule[0][1] == 199850001: #our own change is greater than zero but less than dust #use the same edge case as for negative change, don't add dummy inputs #(because we need tx creation to complete), but trigger case by #bumping dust threshold jm_single().BITCOIN_DUST_THRESHOLD = 14000 res = taker.receive_utxos(maker_response) #should have succeeded to build tx assert res[0] #change should be none assert not taker.my_change_addr return clean_up() if schedule[0][1] == 199599800: #need to force negative fees to make this feasible for k, v in taker.orderbook.iteritems(): v['cjfee'] = '-0.002' # change_amount = (total_input - self.cjamount - # self.orderbook[nick]['txfee'] + real_cjfee) #suppose change amount is 1000 (sub dust), then solve for x; #given that real_cjfee = -0.002*x #change = 200000000 - x - 1000 - 0.002*x #x*1.002 = 1999999000; x = 199599800 res = taker.receive_utxos(maker_response) assert not res[0] assert res[ 1] == "Not enough counterparties responded to fill, giving up" return clean_up() if schedule[0][3] == "mxeLuX8PP7qLkcM8uarHmdZyvP1b5e1Ynf": #to trigger rounding error for sweep (change non-zero), #modify the total_input via the values in self.input_utxos; #the amount to trigger a 2 satoshi change is found by trial-error. #TODO note this test is not adequate, because the code is not; #the code does not *DO* anything if a condition is unexpected. taker.input_utxos = copy.deepcopy(t_utxos_by_mixdepth)[0] for k, v in taker.input_utxos.iteritems(): v["value"] = int(0.999805228 * v["value"]) res = taker.receive_utxos(maker_response) assert res[0] return clean_up() res = taker.receive_utxos(maker_response) if minmakers != 2: assert not res[0] assert res[ 1] == "Not enough counterparties responded to fill, giving up" return clean_up() assert res[0] #re-calling will trigger "finished" code, since schedule is "complete". res = taker.initialize(orderbook) assert not res[0] #some exception cases: no coinjoin address, no change address: #donations not yet implemented: taker.my_cj_addr = None with pytest.raises(NotImplementedError) as e_info: taker.prepare_my_bitcoin_data() with pytest.raises(NotImplementedError) as e_info: taker.sign_tx("a", "b", "c", "d") with pytest.raises(NotImplementedError) as e_info: a = taker.coinjoin_address() taker.wallet.inject_addr_get_failure = True taker.my_cj_addr = "dummy" assert not taker.prepare_my_bitcoin_data() #clean up return clean_up()
def test_wrong_network_bci(setup_wallets): rpc = jm_single().bc_interface.jsonRpc with pytest.raises(Exception) as e_info: x = BitcoinCoreInterface(rpc, 'mainnet')
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 = SegwitWallet(wallet_name, None, max_mix_depth) else: while True: try: pwd = get_password("Enter wallet decryption passphrase: ") wallet = SegwitWallet(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 test_create_psbt_and_sign(setup_psbt_wallet, unowned_utxo, wallet_cls): """ Plan of test: 1. Create a wallet and source 3 destination addresses. 2. Make, and confirm, transactions that fund the 3 addrs. 3. Create a new tx spending 2 of those 3 utxos and spending another utxo we don't own (extra is optional per `unowned_utxo`). 4. Create a psbt using the above transaction and corresponding `spent_outs` field to fill in the redeem script. 5. Compare resulting PSBT with expected structure. 6. Use the wallet's sign_psbt method to sign the whole psbt, which means signing each input we own. 7. Check that each input is finalized as per expected. Check that the whole PSBT is or is not finalized as per whether there is an unowned utxo. 8. In case where whole psbt is finalized, attempt to broadcast the tx. """ # steps 1 and 2: wallet_service = make_wallets(1, [[3, 0, 0, 0, 0]], 1, wallet_cls=wallet_cls)[0]['wallet'] wallet_service.sync_wallet(fast=True) utxos = wallet_service.select_utxos(0, bitcoin.coins_to_satoshi(1.5)) # for legacy wallets, psbt creation requires querying for the spending # transaction: if wallet_cls == LegacyWallet: fulltxs = [] for utxo, v in utxos.items(): fulltxs.append( jm_single().bc_interface.get_deser_from_gettransaction( jm_single().bc_interface.get_transaction(utxo[0]))) assert len(utxos) == 2 u_utxos = {} if unowned_utxo: # note: tx creation uses the key only; psbt creation uses the value, # which can be fake here; we do not intend to attempt to fully # finalize a psbt with an unowned input. See # https://github.com/Simplexum/python-bitcointx/issues/30 # the redeem script creation (which is artificial) will be # avoided in future. priv = b"\xaa" * 32 + b"\x01" pub = bitcoin.privkey_to_pubkey(priv) script = bitcoin.pubkey_to_p2sh_p2wpkh_script(pub) redeem_script = bitcoin.pubkey_to_p2wpkh_script(pub) u_utxos[(b"\xaa" * 32, 12)] = {"value": 1000, "script": script} utxos.update(u_utxos) # outputs aren't interesting for this test (we selected 1.5 but will get 2): outs = [{ "value": bitcoin.coins_to_satoshi(1.999), "address": wallet_service.get_addr(0, 0, 0) }] tx = bitcoin.mktx(list(utxos.keys()), outs) if wallet_cls != LegacyWallet: spent_outs = wallet_service.witness_utxos_to_psbt_utxos(utxos) force_witness_utxo = True else: spent_outs = fulltxs # the extra input is segwit: if unowned_utxo: spent_outs.extend( wallet_service.witness_utxos_to_psbt_utxos(u_utxos)) force_witness_utxo = False newpsbt = wallet_service.create_psbt_from_tx( tx, spent_outs, force_witness_utxo=force_witness_utxo) # see note above if unowned_utxo: newpsbt.inputs[-1].redeem_script = redeem_script print(bintohex(newpsbt.serialize())) print("human readable: ") print(wallet_service.human_readable_psbt(newpsbt)) # we cannot compare with a fixed expected result due to wallet randomization, but we can # check psbt structure: expected_inputs_length = 3 if unowned_utxo else 2 assert len(newpsbt.inputs) == expected_inputs_length assert len(newpsbt.outputs) == 1 # note: redeem_script field is a CScript which is a bytes instance, # so checking length is best way to check for existence (comparison # with None does not work): if wallet_cls == SegwitLegacyWallet: assert len(newpsbt.inputs[0].redeem_script) != 0 assert len(newpsbt.inputs[1].redeem_script) != 0 if unowned_utxo: assert newpsbt.inputs[2].redeem_script == redeem_script signed_psbt_and_signresult, err = wallet_service.sign_psbt( newpsbt.serialize(), with_sign_result=True) assert err is None signresult, signed_psbt = signed_psbt_and_signresult expected_signed_inputs = len(utxos) if not unowned_utxo else len(utxos) - 1 assert signresult.num_inputs_signed == expected_signed_inputs assert signresult.num_inputs_final == expected_signed_inputs if not unowned_utxo: assert signresult.is_final # only in case all signed do we try to broadcast: extracted_tx = signed_psbt.extract_transaction().serialize() assert jm_single().bc_interface.pushtx(extracted_tx) else: # transaction extraction must fail for not-fully-signed psbts: with pytest.raises(ValueError) as e: extracted_tx = signed_psbt.extract_transaction()
def do_GET(self): # http.server.SimpleHTTPRequestHandler.do_GET(self) # print 'httpd received ' + self.path + ' request' self.path, query = self.path.split( '?', 1) if '?' in self.path else (self.path, '') args = parse_qs(query) pages = ['/', '/ordersize', '/depth', '/orderbook.json'] if self.path not in pages: return fd = open( os.path.join(os.path.dirname(os.path.realpath(__file__)), 'orderbook.html'), 'r') orderbook_fmt = fd.read() fd.close() alert_msg = '' if jm_single().joinmarket_alert[0]: alert_msg = '<br />JoinMarket Alert Message:<br />' + \ jm_single().joinmarket_alert[0] if self.path == '/': btc_unit = args['btcunit'][ 0] if 'btcunit' in args else sorted_units[0] rel_unit = args['relunit'][ 0] if 'relunit' in args else sorted_rel_units[0] if btc_unit not in sorted_units: btc_unit = sorted_units[0] if rel_unit not in sorted_rel_units: rel_unit = sorted_rel_units[0] ordercount, ordertable = self.create_orderbook_table( btc_unit, rel_unit) choose_units_form = create_choose_units_form(btc_unit, rel_unit) table_heading = create_table_heading(btc_unit, rel_unit) replacements = { 'PAGETITLE': 'JoinMarket Browser Interface', 'MAINHEADING': 'JoinMarket Orderbook', 'SECONDHEADING': (str(ordercount) + ' orders found by ' + self.get_counterparty_count() + ' counterparties' + alert_msg), 'MAINBODY': (rotateObform + refresh_orderbook_form + choose_units_form + table_heading + ordertable + '</table>\n') } elif self.path == '/ordersize': replacements = { 'PAGETITLE': 'JoinMarket Browser Interface', 'MAINHEADING': 'Order Sizes', 'SECONDHEADING': 'Order Size Histogram' + alert_msg, 'MAINBODY': self.create_size_histogram(args) } elif self.path.startswith('/depth'): # if self.path[6] == '?': # quantity = cj_amounts = [10**cja for cja in range(4, 12, 1)] mainbody = [self.create_depth_chart(cja, args) \ for cja in cj_amounts] + \ ["<br/><a href='?'>linear</a>" if args.get("scale") \ else "<br/><a href='?scale=log'>log scale</a>"] replacements = { 'PAGETITLE': 'JoinMarket Browser Interface', 'MAINHEADING': 'Depth Chart', 'SECONDHEADING': 'Orderbook Depth' + alert_msg, 'MAINBODY': '<br />'.join(mainbody) } elif self.path == '/orderbook.json': replacements = {} orderbook_fmt = json.dumps(self.create_orderbook_obj()) orderbook_page = orderbook_fmt for key, rep in iteritems(replacements): orderbook_page = orderbook_page.replace(key, rep) self.send_response(200) if self.path.endswith('.json'): self.send_header('Content-Type', 'application/json') else: self.send_header('Content-Type', 'text/html') self.send_header('Content-Length', len(orderbook_page)) self.end_headers() self.wfile.write(orderbook_page.encode('utf-8'))
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) check_and_start_tor() #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 < jm_single().DUST_THRESHOLD: jmprint( 'ERROR: Amount ' + btc.amount_to_str(amount) + ' is below dust threshold ' + btc.amount_to_str(jm_single().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)) custom_change = None if options.customchange != '': addr_valid, errormsg = validate_address(options.customchange) if not addr_valid: parser.error( "The custom change address provided is not valid\n{}".format( errormsg)) sys.exit(EXIT_ARGERROR) custom_change = options.customchange if destaddr and custom_change == destaddr: parser.error("The custom change address cannot be the same as the " "destination address.") sys.exit(EXIT_ARGERROR) if sweeping: parser.error(sweep_custom_change_warning) sys.exit(EXIT_ARGERROR) if bip78url: parser.error( "Custom change is not currently supported " "with Payjoin. Please retry without a custom change address.") sys.exit(EXIT_ARGERROR) if options.makercount > 0: if not options.answeryes and input(general_custom_change_warning + " (y/n):")[0] != "y": sys.exit(EXIT_ARGERROR) engine_recognized = True try: change_addr_type = wallet_service.get_outtype(custom_change) except EngineError: engine_recognized = False if (not engine_recognized) or (change_addr_type != wallet_service.get_txtype()): if not options.answeryes and input( nonwallet_custom_change_warning + " (y/n):")[0] != "y": sys.exit(EXIT_ARGERROR) if options.makercount == 0 and not bip78url: tx = direct_send(wallet_service, amount, mixdepth, destaddr, options.answeryes, with_final_psbt=options.with_psbt, optin_rbf=options.rbf, custom_change_addr=custom_change) 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() nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False dhost = jm_single().config.get("DAEMON", "daemon_host") dport = jm_single().config.getint("DAEMON", "daemon_port") if bip78url: # TODO sanity check wallet type is segwit manager = parse_payjoin_setup(args[1], wallet_service, options.mixdepth) reactor.callWhenRunning(send_payjoin, manager) # JM is default, so must be switched off explicitly in this call: start_reactor(dhost, dport, bip78=True, jm_coinjoin=False, daemon=daemon) return else: taker = Taker(wallet_service, schedule, order_chooser=chooseOrdersFunc, max_cj_fee=maxcjfee, callbacks=(filter_orders_callback, None, taker_finished), custom_change_address=custom_change) clientfactory = JMClientProtocolFactory(taker) if jm_single().config.get("BLOCKCHAIN", "network") == "regtest": startLogging(sys.stdout) start_reactor(dhost, dport, clientfactory, daemon=daemon)
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')) (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() if not os.path.exists(os.path.join('wallets', wallet_name)): wallet = get_wallet_cls()(wallet_name, None, max_mix_depth=MAX_MIX_DEPTH, gaplimit=options.gaplimit) else: while True: try: pwd = get_password("Enter wallet decryption passphrase: ") wallet = get_wallet_cls()(wallet_name, pwd, max_mix_depth=MAX_MIX_DEPTH, gaplimit=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 if jm_single().config.get("BLOCKCHAIN", "blockchain_source") == "electrum-server": jm_single().bc_interface.synctype = "with-script" 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 wallet_tool_main(wallet_root_path): """Main wallet tool script function; returned is a string (output or error) """ parser = get_wallettool_parser() (options, args) = parser.parse_args() walletclass = SegwitWallet if jm_single().config.get( "POLICY", "segwit") == "true" else Wallet # if the index_cache stored in wallet.json is longer than the default # then set maxmixdepth to the length of index_cache maxmixdepth_configured = True if not options.maxmixdepth: maxmixdepth_configured = False options.maxmixdepth = 5 noseed_methods = ['generate', 'recover'] methods = [ 'display', 'displayall', 'summary', 'showseed', 'importprivkey', 'history', 'showutxos' ] methods.extend(noseed_methods) noscan_methods = [ 'showseed', 'importprivkey', 'dumpprivkey', 'signmessage' ] if len(args) < 1: parser.error('Needs a wallet file or method') sys.exit(0) if args[0] in noseed_methods: method = args[0] else: seed = args[0] method = ('display' if len(args) == 1 else args[1].lower()) if not os.path.exists(os.path.join(wallet_root_path, seed)): wallet = walletclass(seed, None, options.maxmixdepth, options.gaplimit, extend_mixdepth=not maxmixdepth_configured, storepassword=(method == 'importprivkey'), wallet_dir=wallet_root_path) else: while True: try: pwd = get_password("Enter wallet decryption passphrase: ") wallet = walletclass( seed, pwd, options.maxmixdepth, options.gaplimit, extend_mixdepth=not maxmixdepth_configured, storepassword=(method == 'importprivkey'), wallet_dir=wallet_root_path) 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 method not in noscan_methods: # if nothing was configured, we override bitcoind's options so that # unconfirmed balance is included in the wallet display by default if 'listunspent_args' not in jm_single().config.options('POLICY'): jm_single().config.set('POLICY', 'listunspent_args', '[0]') sync_wallet(wallet, fast=options.fastsync) #Now the wallet/data is prepared, execute the script according to the method if method == "display": return wallet_display(wallet, options.gaplimit, options.showprivkey) elif method == "displayall": return wallet_display(wallet, options.gaplimit, options.showprivkey, displayall=True) elif method == "summary": return wallet_display(wallet, options.gaplimit, options.showprivkey, summarized=True) elif method == "history": if not isinstance(jm_single().bc_interface, BitcoinCoreInterface): print( 'showing history only available when using the Bitcoin Core ' + 'blockchain interface') sys.exit(0) else: return wallet_fetch_history(wallet, options) elif method == "generate": retval = wallet_generate_recover("generate", wallet_root_path) return retval if retval else "Failed" elif method == "recover": retval = wallet_generate_recover("recover", wallet_root_path) return retval if retval else "Failed" elif method == "showutxos": return wallet_showutxos(wallet, options.showprivkey) elif method == "showseed": return wallet_showseed(wallet) elif method == "dumpprivkey": return wallet_dumpprivkey(wallet, options.hd_path) elif method == "importprivkey": #note: must be interactive (security) wallet_importprivkey(wallet, options.mixdepth) return "Key import completed." elif method == "signmessage": return wallet_signmessage(wallet, options.hd_path, args[1])
# (1) TLS clearnet server # (0) onion non-SSL server # so the third argument is 0 or 1 as per that. # the 4th argument, serverport, is required for (0), # since it's an ephemeral HS address and must include the port # Note on setting up the Hidden Service: # this happens automatically when running test/payjoinserver.py # under pytest, and it prints out the hidden service url after # some seconds (just as it prints out the wallet hex). usessl = int(sys.argv[3]) serverport = None if len(sys.argv) > 4: serverport = sys.argv[4] load_test_config() jm_single().datadir = "." check_regtest() if not usessl: if not serverport: print("test configuration error: usessl = 0 assumes onion " "address which must be specified as the fourth argument") else: pjurl = "http://" + serverport else: # hardcoded port for tests: pjurl = "https://127.0.0.1:8080" bip21uri = "bitcoin:2N7CAdEUjJW9tUHiPhDkmL9ukPtcukJMoxK?amount=0.3&pj=" + pjurl wallet_path = get_wallet_path(wallet_name, None) if jm_single().config.get("POLICY", "native") == "true": walletclass = SegwitWallet
def start_reactor(host, port, factory=None, snickerfactory=None, bip78=False, jm_coinjoin=True, ish=True, daemon=False, rs=True, gui=False): #pragma: no cover #(Cannot start the reactor in tests) #Not used in prod (twisted logging): #startLogging(stdout) usessl = True if jm_single().config.get("DAEMON", "use_ssl") != 'false' else False jmcport, snickerport, bip78port = [port] * 3 if daemon: try: from jmdaemon import JMDaemonServerProtocolFactory, start_daemon, \ SNICKERDaemonServerProtocolFactory, BIP78ServerProtocolFactory except ImportError: jlog.error("Cannot start daemon without jmdaemon package; " "either install it, and restart, or, if you want " "to run the daemon separately, edit the DAEMON " "section of the config. Quitting.") return if jm_coinjoin: dfactory = JMDaemonServerProtocolFactory() if snickerfactory: sdfactory = SNICKERDaemonServerProtocolFactory() if bip78: bip78factory = BIP78ServerProtocolFactory() # ints are immutable in python, to pass by ref we use # an array object: port_a = [port] def start_daemon_on_port(p, f, name, port_offset): orgp = p[0] while True: try: start_daemon(host, p[0] - port_offset, f, usessl, './ssl/key.pem', './ssl/cert.pem') jlog.info("{} daemon listening on port {}".format( name, str(p[0] - port_offset))) break except Exception: jlog.warn("Cannot listen on port " + str(p[0] - port_offset) + ", trying next port") if p[0] >= (orgp + 100): jlog.error("Tried 100 ports but cannot " "listen on any of them. Quitting.") sys.exit(EXIT_FAILURE) p[0] += 1 return p[0] if jm_coinjoin: # TODO either re-apply this port incrementing logic # to other protocols, or re-work how the ports work entirely. jmcport = start_daemon_on_port(port_a, dfactory, "Joinmarket", 0) # (See above) For now these other two are just on ports that are 1K offsets. if snickerfactory: snickerport = start_daemon_on_port(port_a, sdfactory, "SNICKER", 1000) - 1000 if bip78: start_daemon_on_port(port_a, bip78factory, "BIP78", 2000) # if the port had to be incremented due to conflict above, we should update # it in the config var so e.g. bip78 connections choose the port we actually # used. # This is specific to the daemon-in-same-process case; for the external daemon # the user must just set the right value. jm_single().config.set("DAEMON", "daemon_port", str(port_a[0])) # Note the reactor.connect*** entries do not include BIP78 which # starts in jmclient.payjoin: if usessl: if factory: reactor.connectSSL(host, jmcport, factory, ClientContextFactory()) if snickerfactory: reactor.connectSSL(host, snickerport, snickerfactory, ClientContextFactory()) else: if factory: reactor.connectTCP(host, jmcport, factory) if snickerfactory: reactor.connectTCP(host, snickerport, snickerfactory) if rs: if not gui: reactor.run(installSignalHandlers=ish) if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface): jm_single().bc_interface.shutdown_signal = True
def test_external_commitments(setup_podle): """Add this generated commitment to the external list {txid:N:{'P':pubkey, 'reveal':{1:{'P2':P2,'s':s,'e':e}, 2:{..},..}}} Note we do this *after* the sendpayment test so that the external commitments will not erroneously used (they are fake). """ #ensure the file exists even if empty update_commitments() ecs = {} tries = jm_single().config.getint("POLICY", "taker_utxo_retries") for i in range(10): priv = os.urandom(32) dummy_utxo = bitcoin.sha256(priv) + ":2" ecs[dummy_utxo] = {} ecs[dummy_utxo]['reveal'] = {} for j in range(tries): P, P2, s, e, commit = generate_single_podle_sig(priv, j) if 'P' not in ecs[dummy_utxo]: ecs[dummy_utxo]['P'] = P ecs[dummy_utxo]['reveal'][j] = {'P2': P2, 's': s, 'e': e} add_external_commitments(ecs) used, external = get_podle_commitments() for u in external: assert external[u]['P'] == ecs[u]['P'] for i in range(tries): for x in ['P2', 's', 'e']: assert external[u]['reveal'][str( i)][x] == ecs[u]['reveal'][i][x] #add a dummy used commitment, then try again update_commitments(commitment="ab" * 32) ecs = {} known_commits = [] known_utxos = [] tries = 3 for i in range(1, 6): u = binascii.hexlify(struct.pack(b'B', i) * 32).decode('ascii') known_utxos.append(u) priv = struct.pack(b'B', i) * 32 + b"\x01" ecs[u] = {} ecs[u]['reveal'] = {} for j in range(tries): P, P2, s, e, commit = generate_single_podle_sig(priv, j) known_commits.append(commit) if 'P' not in ecs[u]: ecs[u]['P'] = P ecs[u]['reveal'][j] = {'P2': P2, 's': s, 'e': e} add_external_commitments(ecs) #simulate most of those external being already used for c in known_commits[:-1]: update_commitments(commitment=c) #this should find the remaining one utxo and return from it assert generate_podle([], max_tries=tries, allow_external=known_utxos) #test commitment removal to_remove = ecs[binascii.hexlify(struct.pack(b'B', 3) * 32).decode('ascii')] update_commitments(external_to_remove={ binascii.hexlify(struct.pack(b'B', 3) * 32).decode('ascii'): to_remove }) #test that an incorrectly formatted file raises with open(get_commitment_file(), "rb") as f: validjson = json.loads(f.read().decode('utf-8')) corruptjson = copy.deepcopy(validjson) del corruptjson['used'] with open(get_commitment_file(), "wb") as f: f.write(json.dumps(corruptjson, indent=4).encode('utf-8')) with pytest.raises(PoDLEError) as e_info: get_podle_commitments() #clean up with open(get_commitment_file(), "wb") as f: f.write(json.dumps(validjson, indent=4).encode('utf-8'))
def create_my_orders(self): mix_balance = self.get_available_mixdepths() # We publish ONLY the maximum amount and use minsize for lower bound; # leave it to oid_to_order to figure out the right depth to use. f = '0' if self.ordertype in ['swreloffer', 'sw0reloffer']: f = self.cjfee_r elif self.ordertype in ['swabsoffer', 'sw0absoffer']: f = str(self.txfee + self.cjfee_a) mix_balance = dict([(m, b) for m, b in iteritems(mix_balance) if b > self.minsize]) if len(mix_balance) == 0: jlog.error('You do not have the minimum required amount of coins' ' to be a maker: ' + str(minsize)) return [] max_mix = max(mix_balance, key=mix_balance.get) # randomizing the different values randomize_txfee = int( random.uniform(txfee * (1 - float(txfee_factor)), txfee * (1 + float(txfee_factor)))) randomize_minsize = int( random.uniform(self.minsize * (1 - float(size_factor)), self.minsize * (1 + float(size_factor)))) possible_maxsize = mix_balance[max_mix] - max( jm_single().DUST_THRESHOLD, randomize_txfee) randomize_maxsize = int( random.uniform(possible_maxsize * (1 - float(size_factor)), possible_maxsize)) if self.ordertype in ['swabsoffer', 'sw0absoffer']: randomize_cjfee = int( random.uniform( float(cjfee_a) * (1 - float(cjfee_factor)), float(cjfee_a) * (1 + float(cjfee_factor)))) randomize_cjfee = randomize_cjfee + randomize_txfee else: randomize_cjfee = random.uniform( float(f) * (1 - float(cjfee_factor)), float(f) * (1 + float(cjfee_factor))) randomize_cjfee = "{0:.6f}".format( randomize_cjfee) # round to 6 decimals order = { 'oid': 0, 'ordertype': self.ordertype, 'minsize': randomize_minsize, 'maxsize': randomize_maxsize, 'txfee': randomize_txfee, 'cjfee': str(randomize_cjfee) } # sanity check assert order['minsize'] >= 0 assert order['maxsize'] > 0 assert order['minsize'] <= order['maxsize'] if order['ordertype'] in ['swreloffer', 'sw0reloffer']: while order['txfee'] >= (float(order['cjfee']) * order['minsize']): order['txfee'] = int(order['txfee'] / 2) jlog.info( 'Warning: too high txfee to be profitable, halfing it to: ' + str(order['txfee'])) return [order]
def persist_config(file_path): '''This loses all comments in the config file. TODO: possibly correct that.''' with open(file_path, 'w') as f: jm_single().config.write(f)
def main(): parser = OptionParser( usage='usage: %prog [options] [txid:n]', description= "Adds one or more utxos to the list that can be used to make " "commitments for anti-snooping. Note that this utxo, and its " "PUBkey, will be revealed to makers, so consider the privacy " "implication. " "It may be useful to those who are having trouble making " "coinjoins due to several unsuccessful attempts (especially " "if your joinmarket wallet is new). " "'Utxo' means unspent transaction output, it must not " "already be spent. " "The options -w, -r and -R offer ways to load these utxos " "from a file or wallet. " "If you enter a single utxo without these options, you will be " "prompted to enter the private key here - it must be in " "WIF compressed format. " "BE CAREFUL about handling private keys! " "Don't do this in insecure environments. " "Also note this ONLY works for standard (p2pkh or p2sh-p2wpkh) utxos.") parser.add_option( '-r', '--read-from-file', action='store', type='str', dest='in_file', help= 'name of plain text csv file containing utxos, one per line, format: ' 'txid:N, WIF-compressed-privkey') parser.add_option( '-R', '--read-from-json', action='store', type='str', dest='in_json', help= 'name of json formatted file containing utxos with private keys, as ' 'output from "python wallet-tool.py -p walletname showutxos"') parser.add_option( '-w', '--load-wallet', action='store', type='str', dest='loadwallet', help='name of wallet from which to load utxos and use as commitments.') parser.add_option( '-g', '--gap-limit', action='store', type='int', dest='gaplimit', default=6, help= 'Only to be used with -w; gap limit for Joinmarket wallet, default 6.') parser.add_option( '-M', '--max-mixdepth', action='store', type='int', dest='maxmixdepth', default=5, help= 'Only to be used with -w; number of mixdepths for wallet, default 5.') parser.add_option( '-d', '--delete-external', action='store_true', dest='delete_ext', help='deletes the current list of external commitment utxos', default=False) parser.add_option( '-v', '--validate-utxos', action='store_true', dest='validate', help='validate the utxos and pubkeys provided against the blockchain', default=False) parser.add_option( '-o', '--validate-only', action='store_true', dest='vonly', help='only validate the provided utxos (file or command line), not add', default=False) 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')) (options, args) = parser.parse_args() load_program_config() #TODO; sort out "commit file location" global so this script can #run without this hardcoding: utxo_data = [] if options.delete_ext: other = options.in_file or options.in_json or options.loadwallet if len(args) > 0 or other: if raw_input( "You have chosen to delete commitments, other arguments " "will be ignored; continue? (y/n)") != 'y': print "Quitting" sys.exit(0) c, e = get_podle_commitments() print pformat(e) if raw_input( "You will remove the above commitments; are you sure? (y/n): " ) != 'y': print "Quitting" sys.exit(0) update_commitments(external_to_remove=e) print "Commitments deleted." sys.exit(0) #Three options (-w, -r, -R) for loading utxo and privkey pairs from a wallet, #csv file or json file. if options.loadwallet: wallet_path = get_wallet_path(options.loadwallet, None) wallet = open_wallet(wallet_path, gap_limit=options.gaplimit) while not jm_single().bc_interface.wallet_synced: sync_wallet(wallet, fast=options.fastsync) for md, utxos in wallet.get_utxos_by_mixdepth_().items(): for (txid, index), utxo in utxos.items(): txhex = binascii.hexlify(txid) + ':' + str(index) wif = wallet.get_wif_path(utxo['path']) utxo_data.append((txhex, wif)) elif options.in_file: with open(options.in_file, "rb") as f: utxo_info = f.readlines() for ul in utxo_info: ul = ul.rstrip() if ul: u, priv = get_utxo_info(ul) if not u: quit(parser, "Failed to parse utxo info: " + str(ul)) utxo_data.append((u, priv)) elif options.in_json: if not os.path.isfile(options.in_json): print "File: " + options.in_json + " not found." sys.exit(0) with open(options.in_json, "rb") as f: try: utxo_json = json.loads(f.read()) except: print "Failed to read json from " + options.in_json sys.exit(0) for u, pva in utxo_json.iteritems(): utxo_data.append((u, pva['privkey'])) elif len(args) == 1: 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) utxo_data.append((u, priv)) else: quit(parser, 'Invalid syntax') if options.validate or options.vonly: sw = False if jm_single().config.get("POLICY", "segwit") == "false" else True if not validate_utxo_data(utxo_data, segwit=sw): quit(parser, "Utxos did not validate, quitting") if options.vonly: sys.exit(0) #We are adding utxos to the external list assert len(utxo_data) add_ext_commitments(utxo_data)
def clean_up(): jm_single().config.set("POLICY", "taker_utxo_age", old_taker_utxo_age) jm_single().config.set("POLICY", "taker_utxo_amtpercent", old_taker_utxo_amtpercent) set_commitment_file(old_commitment_file) jm_single().bc_interface.setQUSFail(False) os.remove('dummyext')
def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None): if fromtx == "unconfirmed": #If final entry, stop *here*, don't wait for confirmation if taker.schedule_index + 1 == len(taker.schedule): reactor.stop() return if fromtx: if res: txd, txid = txdetails reactor.callLater(waittime * 60, clientfactory.getClient().clientStart) else: #a transaction failed; we'll try to repeat without the #troublemakers. #If this error condition is reached from Phase 1 processing, #and there are less than minimum_makers honest responses, we #just give up (note that in tumbler we tweak and retry, but #for sendpayment the user is "online" and so can manually #try again). #However if the error is in Phase 2 and we have minimum_makers #or more responses, we do try to restart with the honest set, here. if taker.latest_tx is None: #can only happen with < minimum_makers; see above. log.info("A transaction failed but there are insufficient " "honest respondants to continue; giving up.") reactor.stop() return #This is Phase 2; do we have enough to try again? taker.add_honest_makers( list( set(taker.maker_utxo_data.keys()).symmetric_difference( set(taker.nonrespondants)))) if len(taker.honest_makers) < jm_single().config.getint( "POLICY", "minimum_makers"): log.info("Too few makers responded honestly; " "giving up this attempt.") reactor.stop() return jmprint("We failed to complete the transaction. The following " "makers responded honestly: " + str(taker.honest_makers) +\ ", so we will retry with them.", "warning") #Now we have to set the specific group we want to use, and hopefully #they will respond again as they showed honesty last time. #we must reset the number of counterparties, as well as fix who they #are; this is because the number is used to e.g. calculate fees. #cleanest way is to reset the number in the schedule before restart. taker.schedule[taker.schedule_index][2] = len( taker.honest_makers) log.info("Retrying with: " + str(taker.schedule[taker.schedule_index][2]) + " counterparties.") #rewind to try again (index is incremented in Taker.initialize()) taker.schedule_index -= 1 taker.set_honest_only(True) reactor.callLater(5.0, clientfactory.getClient().clientStart) else: if not res: log.info("Did not complete successfully, shutting down") #Should usually be unreachable, unless conf received out of order; #because we should stop on 'unconfirmed' for last (see above) else: log.info("All transactions completed correctly") reactor.stop()
def test_on_sig(setup_taker, dummyaddr, schedule): #plan: create a new transaction with known inputs and dummy outputs; #then, create a signature with various inputs, pass in in b64 to on_sig. #in order for it to verify, the DummyBlockchainInterface will have to #return the right values in query_utxo_set utxos = [(struct.pack(b"B", x) * 32, 1) for x in range(5)] #create 2 privkey + utxos that are to be ours privs = [x*32 + b"\x01" for x in [struct.pack(b'B', y) for y in range(1,6)]] scripts = [BTC_P2PKH.key_to_script(privs[x]) for x in range(5)] addrs = [BTC_P2PKH.privkey_to_address(privs[x]) for x in range(5)] fake_query_results = [{'value': 200000000, 'utxo': utxos[x], 'address': addrs[x], 'script': scripts[x], 'confirms': 20} for x in range(5)] dbci = DummyBlockchainInterface() dbci.insert_fake_query_results(fake_query_results) jm_single().bc_interface = dbci #make a transaction with all the fake results above, and some outputs outs = [{'value': 100000000, 'address': dummyaddr}, {'value': 899990000, 'address': dummyaddr}] tx = bitcoin.mktx(utxos, outs) # since tx will be updated as it is signed, unlike in real life # (where maker signing operation doesn't happen here), we'll create # a second copy without the signatures: tx2 = bitcoin.mktx(utxos, outs) #prepare the Taker with the right intermediate data taker = get_taker(schedule=schedule) taker.nonrespondants=["cp1", "cp2", "cp3"] taker.latest_tx = tx #my inputs are the first 2 utxos taker.input_utxos = {utxos[0]: {'address': addrs[0], 'script': scripts[0], 'value': 200000000}, utxos[1]: {'address': addrs[1], 'script': scripts[1], 'value': 200000000}} taker.utxos = {None: utxos[:2], "cp1": [utxos[2]], "cp2": [utxos[3]], "cp3":[utxos[4]]} for i in range(2): # placeholders required for my inputs taker.latest_tx.vin[i].scriptSig = bitcoin.CScript(hextobin('deadbeef')) tx2.vin[i].scriptSig = bitcoin.CScript(hextobin('deadbeef')) #to prepare for my signing, need to mark cjaddr: taker.my_cj_addr = dummyaddr #make signatures for the last 3 fake utxos, considered as "not ours": sig, msg = bitcoin.sign(tx2, 2, privs[2]) assert sig, "Failed to sign: " + msg sig3 = b64encode(tx2.vin[2].scriptSig) taker.on_sig("cp1", sig3) #try sending the same sig again; should be ignored taker.on_sig("cp1", sig3) sig, msg = bitcoin.sign(tx2, 3, privs[3]) assert sig, "Failed to sign: " + msg sig4 = b64encode(tx2.vin[3].scriptSig) #try sending junk instead of cp2's correct sig assert not taker.on_sig("cp2", str("junk")), "incorrectly accepted junk signature" taker.on_sig("cp2", sig4) sig, msg = bitcoin.sign(tx2, 4, privs[4]) assert sig, "Failed to sign: " + msg #Before completing with the final signature, which will trigger our own #signing, try with an injected failure of query utxo set, which should #prevent this signature being accepted. dbci.setQUSFail(True) sig5 = b64encode(tx2.vin[4].scriptSig) assert not taker.on_sig("cp3", sig5), "incorrectly accepted sig5" #allow it to succeed, and try again dbci.setQUSFail(False) #this should succeed and trigger the we-sign code taker.on_sig("cp3", sig5)
def test_simple_payjoin(monkeypatch, tmpdir, setup_cj, wallet_cls, wallet_structures, mean_amt): def raise_exit(i): raise Exception("sys.exit called") monkeypatch.setattr(sys, 'exit', raise_exit) wallet_services = [] wallet_services.append( make_wallets_to_list( make_wallets(1, wallet_structures=[wallet_structures[0]], mean_amt=mean_amt, wallet_cls=wallet_cls[0]))[0]) wallet_services.append( make_wallets_to_list( make_wallets(1, wallet_structures=[wallet_structures[1]], mean_amt=mean_amt, wallet_cls=wallet_cls[1]))[0]) jm_single().bc_interface.tickchain() sync_wallets(wallet_services) # For accounting purposes, record the balances # at the start. msb = getbals(wallet_services[0], 0) tsb = getbals(wallet_services[1], 0) cj_amount = int(1.1 * 10**8) maker = P2EPMaker(wallet_services[0], 0, cj_amount) destaddr = maker.destination_addr monkeypatch.setattr(maker, 'user_check', dummy_user_check) # TODO use this to sanity check behaviour # in presence of the rest of the joinmarket orderbook. orderbook = create_orderbook([maker]) assert len(orderbook) == 1 # mixdepth, amount, counterparties, dest_addr, waittime; # in payjoin we only pay attention to the first two entries. schedule = [(0, cj_amount, 1, destaddr, 0)] taker = create_taker(wallet_services[-1], schedule, monkeypatch) monkeypatch.setattr(taker, 'user_check', dummy_user_check) init_data = taker.initialize(orderbook) # the P2EPTaker.initialize() returns: # (True, self.cjamount, "p2ep", "p2ep", {self.p2ep_receiver_nick:{}}) assert init_data[0], "taker.initialize error" active_orders = init_data[4] assert len(active_orders.keys()) == 1 response = taker.receive_utxos(list(active_orders.keys())) assert response[0], "taker receive_utxos error" # test for validity of signed fallback transaction; requires 0.17; # note that we count this as an implicit test of fallback mode. res = jm_single().bc_interface.rpc('testmempoolaccept', [[response[2]]]) assert res[0]["allowed"], "Proposed transaction was rejected from mempool." maker_response = maker.on_tx_received("faketaker", response[2]) if not maker_response[0]: print("maker on_tx_received failed, reason: ", maker_response[1]) assert False taker_response = taker.on_tx_received("fakemaker", maker_response[2]) if not taker_response[1] == "OK": print("Failure in taker on_tx_received, reason: ", taker_response[1]) assert False # Although the above OK is proof that a transaction went through, # it doesn't prove it was a good transaction! Here do balance checks: assert final_checks(wallet_services, cj_amount, taker.total_txfee, tsb, msb)
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 setup_wallet(): load_program_config() #see note in cryptoengine.py: cryptoengine.BTC_P2WPKH.VBYTE = 100 jm_single().bc_interface.tick_forward_chain_interval = 2
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)
import logging from twisted.python.log import startLogging from jmclient import (Taker, load_program_config, get_schedule, weighted_order_choose, JMClientProtocolFactory, start_reactor, validate_address, jm_single, WalletError, Wallet, SegwitWallet, sync_wallet, get_tumble_schedule, RegtestBitcoinCoreInterface, estimate_tx_fee, tweak_tumble_schedule, human_readable_schedule_entry, schedule_to_text, restart_waiter, get_tumble_log, tumbler_taker_finished_update, tumbler_filter_orders_callback) from jmbase.support import get_log, debug_dump_object, get_password from cli_options import get_tumbler_parser log = get_log() logsdir = os.path.join(os.path.dirname(jm_single().config_location), "logs") 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 = SegwitWallet(wallet_name, None, max_mix_depth)
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]') 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 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(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() maker = ygclass( wallet_service, [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 setup_wallets(): load_program_config() jm_single().bc_interface.tick_forward_chain_interval = 2
def wallet_fetch_history(wallet, options): # sort txes in a db because python can be really bad with large lists con = sqlite3.connect(":memory:") con.row_factory = sqlite3.Row tx_db = con.cursor() tx_db.execute("CREATE TABLE transactions(txid TEXT, " "blockhash TEXT, blocktime INTEGER);") jm_single().debug_silence[0] = True wallet_name = jm_single().bc_interface.get_wallet_name(wallet) buf = range(1000) t = 0 while len(buf) == 1000: buf = jm_single().bc_interface.rpc('listtransactions', ["*", 1000, t, True]) t += len(buf) tx_data = ((tx['txid'], tx['blockhash'], tx['blocktime']) for tx in buf if 'txid' in tx and 'blockhash' in tx and 'blocktime' in tx) tx_db.executemany('INSERT INTO transactions VALUES(?, ?, ?);', tx_data) txes = tx_db.execute( 'SELECT DISTINCT txid, blockhash, blocktime ' 'FROM transactions ORDER BY blocktime').fetchall() wallet_script_set = set(wallet.get_script_path(p) for p in wallet.yield_known_paths()) def s(): return ',' if options.csv else ' ' def sat_to_str(sat): return '%.8f'%(sat/1e8) def sat_to_str_p(sat): return '%+.8f'%(sat/1e8) def skip_n1(v): return '% 2s'%(str(v)) if v != -1 else ' #' def skip_n1_btc(v): return sat_to_str(v) if v != -1 else '#' + ' '*10 def print_row(index, time, tx_type, amount, delta, balance, cj_n, miner_fees, utxo_count, mixdepth_src, mixdepth_dst, txid): data = [index, datetime.fromtimestamp(time).strftime("%Y-%m-%d %H:%M"), tx_type, sat_to_str(amount), sat_to_str_p(delta), sat_to_str(balance), skip_n1(cj_n), sat_to_str(miner_fees), '% 3d' % utxo_count, skip_n1(mixdepth_src), skip_n1(mixdepth_dst)] if options.verbosity % 2 == 0: data += [txid] print(s().join(map('"{}"'.format, data))) field_names = ['tx#', 'timestamp', 'type', 'amount/btc', 'balance-change/btc', 'balance/btc', 'coinjoin-n', 'total-fees', 'utxo-count', 'mixdepth-from', 'mixdepth-to'] if options.verbosity % 2 == 0: field_names += ['txid'] if options.csv: print('Bumping verbosity level to 4 due to --csv flag') options.verbosity = 4 if options.verbosity > 0: print(s().join(field_names)) if options.verbosity <= 2: cj_batch = [0]*8 + [[]]*2 balance = 0 utxo_count = 0 deposits = [] deposit_times = [] tx_number = 0 for tx in txes: rpctx = jm_single().bc_interface.rpc('gettransaction', [tx['txid']]) txhex = str(rpctx['hex']) txd = btc.deserialize(txhex) output_script_values = {binascii.unhexlify(sv['script']): sv['value'] for sv in txd['outs']} our_output_scripts = wallet_script_set.intersection( output_script_values.keys()) from collections import Counter value_freq_list = sorted(Counter(output_script_values.values()) .most_common(), key=lambda x: -x[1]) non_cj_freq = 0 if len(value_freq_list)==1 else sum(zip( *value_freq_list[1:])[1]) is_coinjoin = (value_freq_list[0][1] > 1 and value_freq_list[0][1] in [non_cj_freq, non_cj_freq+1]) cj_amount = value_freq_list[0][0] cj_n = value_freq_list[0][1] rpc_inputs = [] for ins in txd['ins']: try: wallet_tx = jm_single().bc_interface.rpc('gettransaction', [ins['outpoint']['hash']]) except JsonRpcError: continue input_dict = btc.deserialize(str(wallet_tx['hex']))['outs'][ins[ 'outpoint']['index']] rpc_inputs.append(input_dict) rpc_input_scripts = set(binascii.unhexlify(ind['script']) for ind in rpc_inputs) our_input_scripts = wallet_script_set.intersection(rpc_input_scripts) our_input_values = [ ind['value'] for ind in rpc_inputs if binascii.unhexlify(ind['script']) in our_input_scripts] our_input_value = sum(our_input_values) utxos_consumed = len(our_input_values) tx_type = None amount = 0 delta_balance = 0 fees = 0 mixdepth_src = -1 mixdepth_dst = -1 #TODO this seems to assume all the input addresses are from the same # mixdepth, which might not be true if len(our_input_scripts) == 0 and len(our_output_scripts) > 0: #payment to us amount = sum([output_script_values[a] for a in our_output_scripts]) tx_type = 'deposit ' cj_n = -1 delta_balance = amount mixdepth_dst = tuple(wallet.get_script_mixdepth(a) for a in our_output_scripts) if len(mixdepth_dst) == 1: mixdepth_dst = mixdepth_dst[0] elif len(our_input_scripts) == 0 and len(our_output_scripts) == 0: continue # skip those that don't belong to our wallet elif len(our_input_scripts) > 0 and len(our_output_scripts) == 0: # we swept coins elsewhere if is_coinjoin: tx_type = 'cj sweepout' amount = cj_amount fees = our_input_value - cj_amount else: tx_type = 'sweep out ' amount = sum([v for v in output_script_values.values()]) fees = our_input_value - amount delta_balance = -our_input_value mixdepth_src = wallet.get_script_mixdepth(list(our_input_scripts)[0]) elif len(our_input_scripts) > 0 and len(our_output_scripts) == 1: # payment to somewhere with our change address getting the remaining change_value = output_script_values[list(our_output_scripts)[0]] if is_coinjoin: tx_type = 'cj withdraw' amount = cj_amount else: tx_type = 'withdraw' #TODO does tx_fee go here? not my_tx_fee only? amount = our_input_value - change_value cj_n = -1 delta_balance = change_value - our_input_value fees = our_input_value - change_value - cj_amount mixdepth_src = wallet.get_script_mixdepth(list(our_input_scripts)[0]) elif len(our_input_scripts) > 0 and len(our_output_scripts) == 2: #payment to self out_value = sum([output_script_values[a] for a in our_output_scripts]) if not is_coinjoin: print('this is wrong TODO handle non-coinjoin internal') tx_type = 'cj internal' amount = cj_amount delta_balance = out_value - our_input_value mixdepth_src = wallet.get_script_mixdepth(list(our_input_scripts)[0]) cj_script = list(set([a for a, v in output_script_values.iteritems() if v == cj_amount]).intersection(our_output_scripts))[0] mixdepth_dst = wallet.get_script_mixdepth(cj_script) else: tx_type = 'unknown type' print('our utxos: ' + str(len(our_input_scripts)) \ + ' in, ' + str(len(our_output_scripts)) + ' out') balance += delta_balance utxo_count += (len(our_output_scripts) - utxos_consumed) index = '% 4d'%(tx_number) tx_number += 1 timestamp = datetime.fromtimestamp(rpctx['blocktime'] ).strftime("%Y-%m-%d %H:%M") utxo_count_str = '% 3d' % (utxo_count) if options.verbosity > 0: if options.verbosity <= 2: n = cj_batch[0] if tx_type == 'cj internal': cj_batch[0] += 1 cj_batch[1] += rpctx['blocktime'] cj_batch[2] += amount cj_batch[3] += delta_balance cj_batch[4] = balance cj_batch[5] += cj_n cj_batch[6] += fees cj_batch[7] += utxo_count cj_batch[8] += [mixdepth_src] cj_batch[9] += [mixdepth_dst] elif tx_type != 'unknown type': if n > 0: # print the previously-accumulated batch print_row('N='+"%2d"%n, cj_batch[1]/n, 'cj batch ', cj_batch[2], cj_batch[3], cj_batch[4], cj_batch[5]/n, cj_batch[6], cj_batch[7]/n, min(cj_batch[8]), max(cj_batch[9]), '...') cj_batch = [0]*8 + [[]]*2 # reset the batch collector # print batch terminating row print_row(index, rpctx['blocktime'], tx_type, amount, delta_balance, balance, cj_n, fees, utxo_count, mixdepth_src, mixdepth_dst, tx['txid']) elif options.verbosity >= 5 or \ (options.verbosity >= 3 and tx_type != 'unknown type'): print_row(index, rpctx['blocktime'], tx_type, amount, delta_balance, balance, cj_n, fees, utxo_count, mixdepth_src, mixdepth_dst, tx['txid']) if tx_type != 'cj internal': deposits.append(delta_balance) deposit_times.append(rpctx['blocktime']) # we could have a leftover batch! if options.verbosity <= 2: n = cj_batch[0] if n > 0: print_row('N='+"%2d"%n, cj_batch[1]/n, 'cj batch ', cj_batch[2], cj_batch[3], cj_batch[4], cj_batch[5]/n, cj_batch[6], cj_batch[7]/n, min(cj_batch[8]), max(cj_batch[9]), '...') bestblockhash = jm_single().bc_interface.rpc('getbestblockhash', []) try: #works with pruning enabled, but only after v0.12 now = jm_single().bc_interface.rpc('getblockheader', [bestblockhash] )['time'] except JsonRpcError: now = jm_single().bc_interface.rpc('getblock', [bestblockhash])['time'] print(' %s best block is %s' % (datetime.fromtimestamp(now) .strftime("%Y-%m-%d %H:%M"), bestblockhash)) total_profit = float(balance - sum(deposits)) / float(100000000) print('total profit = %.8f BTC' % total_profit) if abs(total_profit) > 0: try: # https://gist.github.com/chris-belcher/647da261ce718fc8ca10 import numpy as np from scipy.optimize import brentq deposit_times = np.array(deposit_times) now -= deposit_times[0] deposit_times -= deposit_times[0] deposits = np.array(deposits) def f(r, deposits, deposit_times, now, final_balance): return np.sum(np.exp((now - deposit_times) / 60.0 / 60 / 24 / 365)**r * deposits) - final_balance r = brentq(f, a=1, b=-1, args=(deposits, deposit_times, now, balance)) print('continuously compounded equivalent annual interest rate = ' + str(r * 100) + ' %') print('(as if yield generator was a bank account)') except ImportError: print('scipy not installed, unable to predict accumulation rate') print('to add it to this virtualenv, use `pip2 install scipy`') total_wallet_balance = sum(wallet.get_balance_by_mixdepth().values()) if balance != total_wallet_balance: print(('BUG ERROR: wallet balance (%s) does not match balance from ' + 'history (%s)') % (sat_to_str(total_wallet_balance), sat_to_str(balance))) wallet_utxo_count = sum(map(len, wallet.get_utxos_by_mixdepth_().values())) if utxo_count != wallet_utxo_count: print(('BUG ERROR: wallet utxo count (%d) does not match utxo count from ' + 'history (%s)') % (wallet_utxo_count, utxo_count))
def test_absurd_fee(setup_wallets): jm_single().config.set("POLICY", "absurd_fee_per_kb", "1000") with pytest.raises(ValueError) as e_info: estimate_tx_fee(10, 2) load_program_config()
def test_on_sig(createcmtdata, dummyaddr, signmethod, schedule): #plan: create a new transaction with known inputs and dummy outputs; #then, create a signature with various inputs, pass in in b64 to on_sig. #in order for it to verify, the DummyBlockchainInterface will have to #return the right values in query_utxo_set #create 2 privkey + utxos that are to be ours privs = [x * 32 + "\x01" for x in [chr(y) for y in range(1, 6)]] utxos = [str(x) * 64 + ":1" for x in range(5)] fake_query_results = [{ 'value': 200000000, 'utxo': utxos[x], 'address': bitcoin.privkey_to_address(privs[x], False, magicbyte=0x6f), 'script': bitcoin.mk_pubkey_script( bitcoin.privkey_to_address(privs[x], False, magicbyte=0x6f)), 'confirms': 20 } for x in range(5)] dbci = DummyBlockchainInterface() dbci.insert_fake_query_results(fake_query_results) jm_single().bc_interface = dbci #make a transaction with all the fake results above, and some outputs outs = [{ 'value': 100000000, 'address': dummyaddr }, { 'value': 899990000, 'address': dummyaddr }] tx = bitcoin.mktx(utxos, outs) de_tx = bitcoin.deserialize(tx) #prepare the Taker with the right intermediate data taker = get_taker(schedule=schedule, sign_method=signmethod) taker.nonrespondants = ["cp1", "cp2", "cp3"] taker.latest_tx = de_tx #my inputs are the first 2 utxos taker.input_utxos = { utxos[0]: { 'address': bitcoin.privkey_to_address(privs[0], False, magicbyte=0x6f), 'value': 200000000 }, utxos[1]: { 'address': bitcoin.privkey_to_address(privs[1], False, magicbyte=0x6f), 'value': 200000000 } } taker.utxos = { None: utxos[:2], "cp1": [utxos[2]], "cp2": [utxos[3]], "cp3": [utxos[4]] } for i in range(2): # placeholders required for my inputs taker.latest_tx['ins'][i]['script'] = 'deadbeef' #to prepare for my signing, need to mark cjaddr: taker.my_cj_addr = dummyaddr #make signatures for the last 3 fake utxos, considered as "not ours": tx3 = bitcoin.sign(tx, 2, privs[2]) sig3 = b64encode( bitcoin.deserialize(tx3)['ins'][2]['script'].decode('hex')) taker.on_sig("cp1", sig3) #try sending the same sig again; should be ignored taker.on_sig("cp1", sig3) tx4 = bitcoin.sign(tx, 3, privs[3]) sig4 = b64encode( bitcoin.deserialize(tx4)['ins'][3]['script'].decode('hex')) #try sending junk instead of cp2's correct sig taker.on_sig("cp2", str("junk")) taker.on_sig("cp2", sig4) tx5 = bitcoin.sign(tx, 4, privs[4]) #Before completing with the final signature, which will trigger our own #signing, try with an injected failure of query utxo set, which should #prevent this signature being accepted. dbci.setQUSFail(True) sig5 = b64encode( bitcoin.deserialize(tx5)['ins'][4]['script'].decode('hex')) taker.on_sig("cp3", sig5) #allow it to succeed, and try again dbci.setQUSFail(False) #this should succeed and trigger the we-sign code taker.on_sig("cp3", sig5)
def clean_up(): jm_single().config.set("POLICY", "minimum_makers", oldminmakers) jm_single().config.set("POLICY", "taker_utxo_retries", oldtakerutxoretries) jm_single().config.set("POLICY", "taker_utxo_amtpercent", oldtakerutxoamtpercent)
def checkOffers(self): """Parse offers and total fee from client protocol, allow the user to agree or decide. """ if not self.offers_fee: JMQtMessageBox(self, "Not enough matching offers found.", mbtype='warn', title="Error") self.giveUp() return offers, total_cj_fee = self.offers_fee total_fee_pc = 1.0 * total_cj_fee / self.taker.cjamount #reset the btc amount display string if it's a sweep: if self.cjamount == 0: self.btc_amount_str = str((Decimal(self.taker.cjamount) / Decimal('1e8') )) + " BTC" #TODO separate this out into a function mbinfo = [] if joinmarket_alert[0]: mbinfo.append("<b><font color=red>JOINMARKET ALERT: " + joinmarket_alert[0] + "</font></b>") mbinfo.append(" ") if core_alert[0]: mbinfo.append("<b><font color=red>BITCOIN CORE ALERT: " + core_alert[0] + "</font></b>") mbinfo.append(" ") mbinfo.append("Sending amount: " + self.btc_amount_str) mbinfo.append("to address: " + self.destaddr) mbinfo.append(" ") mbinfo.append("Counterparties chosen:") mbinfo.append('Name, Order id, Coinjoin fee (sat.)') for k, o in offers.iteritems(): if o['ordertype'] == 'reloffer': display_fee = int(self.taker.cjamount * float(o['cjfee'])) - int(o['txfee']) elif o['ordertype'] == 'absoffer': display_fee = int(o['cjfee']) - int(o['txfee']) else: log.debug("Unsupported order type: " + str(o['ordertype']) + ", aborting.") self.giveUp() return False mbinfo.append(k + ', ' + str(o['oid']) + ', ' + str( display_fee)) mbinfo.append('Total coinjoin fee = ' + str(total_cj_fee) + ' satoshis, or ' + str(float('%.3g' % ( 100.0 * total_fee_pc))) + '%') title = 'Check Transaction' if total_fee_pc * 100 > jm_single().config.getint("GUI", "check_high_fee"): title += ': WARNING: Fee is HIGH!!' reply = JMQtMessageBox(self, '\n'.join([m + '<p>' for m in mbinfo]), mbtype='question', title=title) if reply == QMessageBox.Yes: self.filter_offers_response = "ACCEPT" else: self.filter_offers_response = "REJECT" self.giveUp()
def wallet_tool_main(wallet_root_path): """Main wallet tool script function; returned is a string (output or error) """ parser = get_wallettool_parser() (options, args) = parser.parse_args() noseed_methods = ['generate', 'recover'] methods = ['display', 'displayall', 'summary', 'showseed', 'importprivkey', 'history', 'showutxos'] methods.extend(noseed_methods) noscan_methods = ['showseed', 'importprivkey', 'dumpprivkey', 'signmessage'] readonly_methods = ['display', 'displayall', 'summary', 'showseed', 'history', 'showutxos', 'dumpprivkey', 'signmessage'] if len(args) < 1: parser.error('Needs a wallet file or method') sys.exit(0) if options.mixdepth is not None and options.mixdepth < 0: parser.error("Must have at least one mixdepth.") sys.exit(0) if args[0] in noseed_methods: method = args[0] if options.mixdepth is None: options.mixdepth = DEFAULT_MIXDEPTH else: seed = args[0] wallet_path = get_wallet_path(seed, wallet_root_path) method = ('display' if len(args) == 1 else args[1].lower()) read_only = method in readonly_methods wallet = open_test_wallet_maybe( wallet_path, seed, options.mixdepth, read_only=read_only, gap_limit=options.gaplimit) if method not in noscan_methods: # if nothing was configured, we override bitcoind's options so that # unconfirmed balance is included in the wallet display by default if 'listunspent_args' not in jm_single().config.options('POLICY'): jm_single().config.set('POLICY','listunspent_args', '[0]') while not jm_single().bc_interface.wallet_synced: sync_wallet(wallet, fast=options.fastsync) #Now the wallet/data is prepared, execute the script according to the method if method == "display": return wallet_display(wallet, options.gaplimit, options.showprivkey) elif method == "displayall": return wallet_display(wallet, options.gaplimit, options.showprivkey, displayall=True) elif method == "summary": return wallet_display(wallet, options.gaplimit, options.showprivkey, summarized=True) elif method == "history": if not isinstance(jm_single().bc_interface, BitcoinCoreInterface): print('showing history only available when using the Bitcoin Core ' + 'blockchain interface') sys.exit(0) else: return wallet_fetch_history(wallet, options) elif method == "generate": retval = wallet_generate_recover("generate", wallet_root_path, mixdepth=options.mixdepth) return retval if retval else "Failed" elif method == "recover": retval = wallet_generate_recover("recover", wallet_root_path, mixdepth=options.mixdepth) return retval if retval else "Failed" elif method == "showutxos": return wallet_showutxos(wallet, options.showprivkey) elif method == "showseed": return wallet_showseed(wallet) elif method == "dumpprivkey": return wallet_dumpprivkey(wallet, options.hd_path) elif method == "importprivkey": #note: must be interactive (security) if options.mixdepth is None: parser.error("You need to specify a mixdepth with -m") wallet_importprivkey(wallet, options.mixdepth, map_key_type(options.key_type)) return "Key import completed." elif method == "signmessage": return wallet_signmessage(wallet, options.hd_path, args[2])
def startSendPayment(self, ignored_makers=None): if not self.validateSettings(): return #all settings are valid; start #If sweep was requested, make sure the user knew it. self.cjamount = self.widgets[3][1].get_amount() if self.cjamount == 0: mbinfo = ["You selected amount zero, which means 'sweep'."] mbinfo.append("This will spend ALL coins in your wallet to") mbinfo.append("the destination, after fees. Are you sure?") reply = JMQtMessageBox(self, '\n'.join([m + '<p>' for m in mbinfo]), mbtype='question', title='Sweep?') if reply == QMessageBox.No: self.giveUp() return self.startButton.setEnabled(False) self.abortButton.setEnabled(True) log.debug('starting sendpayment') self.destaddr = str(self.widgets[0][1].text()) #inherit format from BTCAmountEdit self.btc_amount_str = str(self.widgets[3][1].text( )) + " " + self.widgets[3][1]._base_unit() makercount = int(self.widgets[1][1].text()) if self.plugin.wallet.has_password(): msg = [] msg.append(_("Enter your password to proceed")) self.plugin.wrap_wallet.password = self.plugin.window.password_dialog( '\n'.join(msg)) try: self.plugin.wallet.check_password( self.plugin.wrap_wallet.password) except Exception as e: JMQtMessageBox(self, "Wrong password: "******"wallet", callbacks=[self.callback_checkOffers, self.callback_takerInfo, self.callback_takerFinished]) if ignored_makers: self.taker.ignored_makers.extend(ignored_makers) if not self.clientfactory: #First run means we need to start: create clientfactory #and start reactor Thread self.clientfactory = JMTakerClientProtocolFactory(self.taker) thread = TaskThread(self) thread.add(partial(start_reactor, "localhost", jm_single().config.getint("GUI", "daemon_port"), self.clientfactory, ish=False)) else: #load the new Taker; TODO this code crashes if daemon port #is changed during run. self.clientfactory.getClient().taker = self.taker self.clientfactory.getClient().clientStart() self.showStatusBarMsg("Connecting to IRC ...")
def test_payjoin_workflow(setup_psbt_wallet, payment_amt, wallet_cls_sender, wallet_cls_receiver): """ Workflow step 1: Create a payment from a wallet, and create a finalized PSBT. This step is fairly trivial as the functionality is built-in to PSBTWalletMixin. Note that only Segwit* wallets are supported for PayJoin. Workflow step 2: Receiver creates a new partially signed PSBT with the same amount and at least one more utxo. Workflow step 3: Given a partially signed PSBT created by a receiver, here the sender completes (co-signs) the PSBT they are given. Note this code is a PSBT functionality check, and does NOT include the detailed checks that the sender should perform before agreeing to sign (see: https://github.com/btcpayserver/btcpayserver-doc/blob/eaac676866a4d871eda5fd7752b91b88fdf849ff/Payjoin-spec.md#receiver-side ). """ wallet_r = make_wallets(1, [[3, 0, 0, 0, 0]], 1, wallet_cls=wallet_cls_receiver)[0]["wallet"] wallet_s = make_wallets(1, [[3, 0, 0, 0, 0]], 1, wallet_cls=wallet_cls_sender)[0]["wallet"] for w in [wallet_r, wallet_s]: w.sync_wallet(fast=True) # destination address for payment: destaddr = str( bitcoin.CCoinAddress.from_scriptPubKey( bitcoin.pubkey_to_p2wpkh_script( bitcoin.privkey_to_pubkey(b"\x01" * 33)))) payment_amt = bitcoin.coins_to_satoshi(payment_amt) # *** STEP 1 *** # ************** # create a normal tx from the sender wallet: payment_psbt = direct_send(wallet_s, payment_amt, 0, destaddr, accept_callback=dummy_accept_callback, info_callback=dummy_info_callback, with_final_psbt=True) print("Initial payment PSBT created:\n{}".format( wallet_s.human_readable_psbt(payment_psbt))) # ensure that the payemnt amount is what was intended: out_amts = [x.nValue for x in payment_psbt.unsigned_tx.vout] # NOTE this would have to change for more than 2 outputs: assert any([out_amts[i] == payment_amt for i in [0, 1]]) # ensure that we can actually broadcast the created tx: # (note that 'extract_transaction' represents an implicit # PSBT finality check). extracted_tx = payment_psbt.extract_transaction().serialize() # don't want to push the tx right now, because of test structure # (in production code this isn't really needed, we will not # produce invalid payment transactions). res = jm_single().bc_interface.testmempoolaccept(bintohex(extracted_tx)) assert res[0]["allowed"], "Payment transaction was rejected from mempool." # *** STEP 2 *** # ************** # Simple receiver utxo choice heuristic. # For more generality we test with two receiver-utxos, not one. all_receiver_utxos = wallet_r.get_all_utxos() # TODO is there a less verbose way to get any 2 utxos from the dict? receiver_utxos_keys = list(all_receiver_utxos.keys())[:2] receiver_utxos = { k: v for k, v in all_receiver_utxos.items() if k in receiver_utxos_keys } # receiver will do other checks as discussed above, including payment # amount; as discussed above, this is out of the scope of this PSBT test. # construct unsigned tx for payjoin-psbt: payjoin_tx_inputs = [(x.prevout.hash[::-1], x.prevout.n) for x in payment_psbt.unsigned_tx.vin] payjoin_tx_inputs.extend(receiver_utxos.keys()) # find payment output and change output pay_out = None change_out = None for o in payment_psbt.unsigned_tx.vout: jm_out_fmt = { "value": o.nValue, "address": str(bitcoin.CCoinAddress.from_scriptPubKey(o.scriptPubKey)) } if o.nValue == payment_amt: assert pay_out is None pay_out = jm_out_fmt else: assert change_out is None change_out = jm_out_fmt # we now know there were two outputs and know which is payment. # bump payment output with our input: outs = [pay_out, change_out] our_inputs_val = sum([v["value"] for _, v in receiver_utxos.items()]) pay_out["value"] += our_inputs_val print("we bumped the payment output value by: ", our_inputs_val) print("It is now: ", pay_out["value"]) unsigned_payjoin_tx = bitcoin.make_shuffled_tx( payjoin_tx_inputs, outs, version=payment_psbt.unsigned_tx.nVersion, locktime=payment_psbt.unsigned_tx.nLockTime) print("we created this unsigned tx: ") print(bitcoin.human_readable_transaction(unsigned_payjoin_tx)) # to create the PSBT we need the spent_outs for each input, # in the right order: spent_outs = [] for i, inp in enumerate(unsigned_payjoin_tx.vin): input_found = False for j, inp2 in enumerate(payment_psbt.unsigned_tx.vin): if inp.prevout == inp2.prevout: spent_outs.append(payment_psbt.inputs[j].utxo) input_found = True break if input_found: continue # if we got here this input is ours, we must find # it from our original utxo choice list: for ru in receiver_utxos.keys(): if (inp.prevout.hash[::-1], inp.prevout.n) == ru: spent_outs.append( wallet_r.witness_utxos_to_psbt_utxos( {ru: receiver_utxos[ru]})[0]) input_found = True break # there should be no other inputs: assert input_found r_payjoin_psbt = wallet_r.create_psbt_from_tx(unsigned_payjoin_tx, spent_outs=spent_outs) print("Receiver created payjoin PSBT:\n{}".format( wallet_r.human_readable_psbt(r_payjoin_psbt))) signresultandpsbt, err = wallet_r.sign_psbt(r_payjoin_psbt.serialize(), with_sign_result=True) assert not err, err signresult, receiver_signed_psbt = signresultandpsbt assert signresult.num_inputs_final == len(receiver_utxos) assert not signresult.is_final print("Receiver signing successful. Payjoin PSBT is now:\n{}".format( wallet_r.human_readable_psbt(receiver_signed_psbt))) # *** STEP 3 *** # ************** # take the half-signed PSBT, validate and co-sign: signresultandpsbt, err = wallet_s.sign_psbt( receiver_signed_psbt.serialize(), with_sign_result=True) assert not err, err signresult, sender_signed_psbt = signresultandpsbt print("Sender's final signed PSBT is:\n{}".format( wallet_s.human_readable_psbt(sender_signed_psbt))) assert signresult.is_final # broadcast the tx extracted_tx = sender_signed_psbt.extract_transaction().serialize() assert jm_single().bc_interface.pushtx(extracted_tx)
def test_minsize_above_maxsize(self): jm_single().DUST_THRESHOLD = 10 yg = create_yg_basic([0, 20000, 10000], txfee_contribution=1000, cjfee_a=10, ordertype='swabsoffer', minsize=100000) self.assertEqual(yg.create_my_orders(), [])