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
Exemple #8
0
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()
Exemple #9
0
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)
Exemple #11
0
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()
Exemple #12
0
 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)
Exemple #14
0
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
Exemple #17
0
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
Exemple #18
0
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)
Exemple #25
0
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)
Exemple #27
0
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
Exemple #28
0
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)
Exemple #31
0
def setup_wallets():
    load_program_config()
    jm_single().bc_interface.tick_forward_chain_interval = 2
Exemple #32
0
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))
Exemple #33
0
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()
Exemple #34
0
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)
Exemple #35
0
 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()
Exemple #37
0
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(), [])