示例#1
0
def make_wallets(n,
                 wallet_structures=None,
                 mean_amt=1,
                 sdev_amt=0,
                 start_index=0,
                 fixed_seeds=None,
                 test_wallet=False,
                 passwords=None,
                 walletclass=SegwitWallet,
                 mixdepths=5):
    '''n: number of wallets to be created
       wallet_structure: array of n arrays , each subarray
       specifying the number of addresses to be populated with coins
       at each depth (for now, this will only populate coins into 'receive' addresses)
       mean_amt: the number of coins (in btc units) in each address as above
       sdev_amt: if randomness in amouts is desired, specify here.
       Returns: a dict of dicts of form {0:{'seed':seed,'wallet':Wallet object},1:..,}
       Default Wallet constructor is joinmarket.Wallet, else use TestWallet,
       which takes a password parameter as in the list passwords.
       '''
    # FIXME: this is basically the same code as jmclient/test/commontest.py
    if len(wallet_structures) != n:
        raise Exception("Number of wallets doesn't match wallet structures")
    if not fixed_seeds:
        seeds = chunks(
            binascii.hexlify(os.urandom(BIP32Wallet.ENTROPY_BYTES * n)),
            BIP32Wallet.ENTROPY_BYTES * 2)
    else:
        seeds = fixed_seeds
    wallets = {}
    for i in range(n):
        assert len(seeds[i]) == BIP32Wallet.ENTROPY_BYTES * 2

        # FIXME: pwd is ignored (but do we really need this anyway?)
        if test_wallet and passwords and i < len(passwords):
            pwd = passwords[i]
        else:
            pwd = None

        w = open_test_wallet_maybe(seeds[i],
                                   seeds[i],
                                   mixdepths - 1,
                                   test_wallet_cls=walletclass)

        wallet_service = WalletService(w)
        wallets[i + start_index] = {
            'seed': seeds[i].decode('ascii'),
            'wallet': wallet_service
        }
        for j in range(mixdepths):
            for k in range(wallet_structures[i][j]):
                deviation = sdev_amt * random.random()
                amt = mean_amt - sdev_amt / 2.0 + deviation
                if amt < 0: amt = 0.001
                amt = float(Decimal(amt).quantize(Decimal(10)**-8))
                jm_single().bc_interface.grab_coins(
                    wallet_service.get_new_addr(
                        j, BaseWallet.ADDRESS_TYPE_INTERNAL), amt)
    return wallets
 def setUp(self):
     global clientfactory
     print("setUp()")
     params = [[False, False], [True, False], [False, True], [-1, False]]
     load_test_config()
     jm_single().maker_timeout_sec = 1
     self.port = reactor.listenTCP(28184, JMTestServerProtocolFactory())
     self.addCleanup(self.port.stopListening)
     def cb(client):
         self.client = client
         self.addCleanup(self.client.transport.loseConnection)
     clientfactories = []
     takers = [DummyTaker(
         WalletService(DummyWallet()), ["a", "b"], default_max_cj_fee, callbacks=(
             None, None, dummy_taker_finished)) for _ in range(len(params))]
     for i, p in enumerate(params):
         takers[i].set_fail_init(p[0])
         takers[i].set_fail_utxos(p[1])
         takers[i].testflag = True
         if i != 0:
             clientfactories.append(JMClientProtocolFactory(takers[i]))
             clientconn = reactor.connectTCP("localhost", 28184,
                                             clientfactories[i])
             self.addCleanup(clientconn.disconnect)
         else:
             clientfactories.append(DummyClientProtocolFactory(takers[i]))
             clientfactory = clientfactories[0]
             clientconn = reactor.connectTCP("localhost", 28184,
                                             clientfactories[0])
             self.addCleanup(clientconn.disconnect)
def test_wallet_mixdepth_decrease(setup_wallet):
    wallet = get_populated_wallet(num=1)

    # setup
    max_mixdepth = wallet.max_mixdepth
    assert max_mixdepth >= 1, "bad default value for mixdepth for this test"
    utxo = fund_wallet_addr(wallet, wallet.get_internal_addr(max_mixdepth), 1)
    assert wallet.get_balance_by_mixdepth()[max_mixdepth] == 10**8
    wallet.close()
    storage_data = wallet._storage.file_data

    # actual test
    new_mixdepth = max_mixdepth - 1
    new_wallet = type(wallet)(VolatileStorage(data=storage_data),
                              mixdepth=new_mixdepth)
    assert new_wallet.max_mixdepth == max_mixdepth
    assert new_wallet.mixdepth == new_mixdepth
    sync_test_wallet(True, WalletService(new_wallet))

    assert max_mixdepth not in new_wallet.get_balance_by_mixdepth()
    assert max_mixdepth not in new_wallet.get_utxos_by_mixdepth()

    # wallet.select_utxos will still return utxos from higher mixdepths
    # because we explicitly ask for a specific mixdepth
    assert utxo in new_wallet.select_utxos_(max_mixdepth, 10**7)
示例#4
0
def test_verify_unsigned_tx_sw_valid(setup_env_nodeps):
    jm_single().config.set("POLICY", "segwit", "true")

    p2sh_gen = address_p2sh_generator()
    p2pkh_gen = address_p2pkh_generator()

    wallet = DummyWallet()
    maker = OfflineMaker(WalletService(wallet))

    cj_addr, cj_script = next(p2sh_gen)
    changeaddr, cj_change_script = next(p2sh_gen)

    # test standard cj
    tx, offerlist = create_tx_and_offerlist(
        cj_addr, changeaddr, [next(p2sh_gen)[0] for s in range(4)])

    assert maker.verify_unsigned_tx(tx, offerlist) == (True,
                                                       None), "standard sw cj"

    # test cj with mixed outputs
    tx, offerlist = create_tx_and_offerlist(
        cj_addr, changeaddr,
        list(
            chain((next(p2sh_gen)[0] for s in range(3)),
                  (next(p2pkh_gen)[0] for s in range(1)))))

    assert maker.verify_unsigned_tx(
        tx, offerlist) == (True, None), "sw cj with p2pkh output"

    # test cj with only p2pkh outputs
    tx, offerlist = create_tx_and_offerlist(
        cj_addr, changeaddr, [next(p2pkh_gen)[0] for s in range(4)])

    assert maker.verify_unsigned_tx(
        tx, offerlist) == (True, None), "sw cj with only p2pkh outputs"
示例#5
0
def test_wallet_mixdepth_decrease(setup_wallet):
    wallet = get_populated_wallet(num=1)

    # setup
    max_mixdepth = wallet.max_mixdepth
    assert max_mixdepth >= 1, "bad default value for mixdepth for this test"
    utxo = fund_wallet_addr(wallet, wallet.get_internal_addr(max_mixdepth), 1)
    bci = jm_single().bc_interface
    unspent_list = bci.listunspent(0)
    # filter on label, but note (a) in certain circumstances (in-
    # wallet transfer) it is possible for the utxo to be labeled
    # with the external label, and (b) the wallet will know if it
    # belongs or not anyway (is_known_addr):
    our_unspent_list = [x for x in unspent_list if (
        bci.is_address_labeled(x, wallet.get_wallet_name()))]
    assert wallet.get_balance_by_mixdepth()[max_mixdepth] == 10**8
    wallet.close()
    storage_data = wallet._storage.file_data

    # actual test
    new_mixdepth = max_mixdepth - 1
    new_wallet = type(wallet)(
        VolatileStorage(data=storage_data), mixdepth=new_mixdepth)
    assert new_wallet.max_mixdepth == max_mixdepth
    assert new_wallet.mixdepth == new_mixdepth
    sync_test_wallet(True, WalletService(new_wallet))

    assert max_mixdepth not in new_wallet.get_balance_by_mixdepth()
    assert max_mixdepth not in new_wallet.get_utxos_by_mixdepth()

    # wallet.select_utxos will still return utxos from higher mixdepths
    # because we explicitly ask for a specific mixdepth
    assert utxo in new_wallet.select_utxos(max_mixdepth, 10**7)
示例#6
0
def make_wallets(n,
                 wallet_structures=None,
                 mean_amt=1,
                 sdev_amt=0,
                 start_index=0,
                 fixed_seeds=None,
                 wallet_cls=SegwitLegacyWallet,
                 mixdepths=5,
                 populate_internal=False):
    '''n: number of wallets to be created
       wallet_structure: array of n arrays , each subarray
       specifying the number of addresses to be populated with coins
       at each depth (for now, this will only populate coins into 'receive' addresses)
       mean_amt: the number of coins (in btc units) in each address as above
       sdev_amt: if randomness in amouts is desired, specify here.
       Returns: a dict of dicts of form {0:{'seed':seed,'wallet':Wallet object},1:..,}
       '''
    # FIXME: this is basically the same code as test/common.py
    assert mixdepths > 0
    if len(wallet_structures) != n:
        raise Exception("Number of wallets doesn't match wallet structures")
    if not fixed_seeds:
        seeds = chunks(
            binascii.hexlify(os.urandom(BIP32Wallet.ENTROPY_BYTES *
                                        n)).decode('ascii'),
            BIP32Wallet.ENTROPY_BYTES * 2)
    else:
        seeds = fixed_seeds
    wallets = {}
    for i in range(n):
        assert len(seeds[i]) == BIP32Wallet.ENTROPY_BYTES * 2

        w = open_test_wallet_maybe(seeds[i],
                                   seeds[i],
                                   mixdepths - 1,
                                   test_wallet_cls=wallet_cls)
        wallet_service = WalletService(w)
        wallets[i + start_index] = {'seed': seeds[i], 'wallet': wallet_service}
        for j in range(mixdepths):
            for k in range(wallet_structures[i][j]):
                deviation = sdev_amt * random.random()
                amt = mean_amt - sdev_amt / 2.0 + deviation
                if amt < 0: amt = 0.001
                amt = float(Decimal(amt).quantize(Decimal(10)**-8))
                jm_single().bc_interface.grab_coins(
                    wallet_service.get_new_addr(j, populate_internal), amt)
    return wallets
def receive_payjoin_main():
    parser = OptionParser(usage='usage: %prog [options] [wallet file] [amount-to-receive]')
    add_base_options(parser)
    parser.add_option('-P', '--hs-port', action='store', type='int',
                      dest='hsport', default=80,
                      help='port on which to serve the ephemeral hidden service.')
    parser.add_option('-g', '--gap-limit', action='store', type="int",
                      dest='gaplimit', default=6,
                      help='gap limit for wallet, default=6')
    parser.add_option('-m', '--mixdepth', action='store', type='int',
                      dest='mixdepth', default=0,
                      help="mixdepth to source coins from")
    parser.add_option('-a',
                      '--amtmixdepths',
                      action='store',
                      type='int',
                      dest='amtmixdepths',
                      help='number of mixdepths in wallet, default 5',
                      default=5)

    (options, args) = parser.parse_args()
    if len(args) < 2:
        parser.error('Needs a wallet, and a receiving amount in bitcoins or satoshis')
        sys.exit(EXIT_ARGERROR)
    wallet_name = args[0]
    try:
        # amount is stored internally in sats, but will be decimal in URL.
        bip78_amount = amount_to_sat(args[1])
    except:
        parser.error("Invalid receiving amount passed: " + bip78_amount)
        sys.exit(EXIT_FAILURE)
    if bip78_amount < 0:
        parser.error("Receiving amount must be a positive number")
        sys.exit(EXIT_FAILURE)
    load_program_config(config_path=options.datadir)

    check_regtest()

    wallet_path = get_wallet_path(wallet_name, None)
    max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1])
    wallet = open_test_wallet_maybe(
        wallet_path, wallet_name, max_mix_depth,
        wallet_password_stdin=options.wallet_password_stdin,
        gap_limit=options.gaplimit)
    wallet_service = WalletService(wallet)

    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=not options.recoversync)
    wallet_service.startService()
    # having enforced wallet sync, we can check if we have coins
    # to do payjoin in the mixdepth
    if wallet_service.get_balance_by_mixdepth()[options.mixdepth] == 0:
        jlog.error("Cannot do payjoin from mixdepth " + str(
            options.mixdepth) + ", no coins. Shutting down.")
        sys.exit(EXIT_ARGERROR)
    receiver_manager = JMBIP78ReceiverManager(wallet_service, options.mixdepth,
                                    bip78_amount, options.hsport)
    receiver_manager.start_pj_server_and_tor()
    reactor.run()
def get_taker(schedule=None, schedule_len=0, on_finished=None,
              filter_orders=None):
    if not schedule:
        #note, for taker.initalize() this will result in junk
        schedule = [['a', 'b', 'c', 'd', 'e', 'f']]*schedule_len
    print("Using schedule: " + str(schedule))
    on_finished_callback = on_finished if on_finished else taker_finished
    filter_orders_callback = filter_orders if filter_orders else dummy_filter_orderbook
    taker = Taker(WalletService(DummyWallet()), schedule, default_max_cj_fee,
                  callbacks=[filter_orders_callback, None, on_finished_callback])
    taker.wallet_service.current_blockheight = 10**6
    return taker
    def initialize_wallet_service(self, request, wallet, wallet_name,
                                  **kwargs):
        """ Called only when the wallet has loaded correctly, so
        authorization is passed, so set cookie for this wallet
        (currently THE wallet, daemon does not yet support multiple).
        This is maintained for 30 minutes currently, or until the user
        switches to a new wallet.
        If an existing wallet_service was in place, it needs to be stopped.
        Here we must also register transaction update callbacks, to fire
        events in the websocket connection.
        """
        if self.services["wallet"]:
            # we allow a new successful authorization (with password)
            # to shut down the currently running service(s), if there
            # are any.
            # This will stop all supporting services and wipe
            # state (so wallet, maker service and cookie/token):
            self.stopSubServices()

        self.services["wallet"] = WalletService(wallet)

        # restart callback needed, otherwise wallet creation will
        # automatically lead to shutdown.
        # TODO: this means that it's possible, in non-standard usage
        # patterns, for the sync to complete without a full record of
        # balances; there are various approaches to passing warnings
        # or requesting rescans, none are implemented yet.
        def dummy_restart_callback(msg):
            jlog.warn("Ignoring rescan request from backend wallet service: " +
                      msg)

        self.services["wallet"].add_restart_callback(dummy_restart_callback)
        self.wallet_name = wallet_name
        self.services["wallet"].register_callbacks(
            [self.wss_factory.sendTxNotification], None)
        self.services["wallet"].startService()
        # now that the WalletService instance is active and ready to
        # respond to requests, we return the status to the client:

        # First, prepare authentication for the calling client:
        self.set_token(wallet_name)
        if ('seedphrase' in kwargs):
            return make_jmwalletd_response(request,
                                           status=201,
                                           walletname=self.wallet_name,
                                           token=self.cookie,
                                           seedphrase=kwargs.get('seedphrase'))
        else:
            return make_jmwalletd_response(request,
                                           walletname=self.wallet_name,
                                           token=self.cookie)
示例#10
0
def test_freeze_timelocked_utxos(setup_env_nodeps):
    storage = VolatileStorage()
    SegwitWalletFidelityBonds.initialize(storage, get_network())
    wallet = SegwitWalletFidelityBonds(storage)
    ts = wallet.datetime_to_time_number(
        datetime.datetime.strptime("2021-07", "%Y-%m"))
    tl_path = wallet.get_path(wallet.FIDELITY_BOND_MIXDEPTH,
                              wallet.BIP32_TIMELOCK_ID, ts)
    tl_script = wallet.get_script_from_path(tl_path)
    utxo = (b'a' * 32, 0)
    wallet.add_utxo(utxo[0], utxo[1], tl_script, 100000000)
    assert not wallet._utxos.is_disabled(*utxo)

    maker = OfflineMaker(WalletService(wallet))
    maker.freeze_timelocked_utxos()
    assert wallet._utxos.is_disabled(*utxo)
示例#11
0
def create_yg_basic(balances, txfee=0, cjfee_a=0, cjfee_r=0,
                    ordertype='swabsoffer', minsize=0):
    """Constructs a YieldGeneratorBasic instance with a fake wallet.  The
    wallet will have the given balances at mixdepths, and the offer params
    will be set as given here."""

    wallet = CustomUtxoWallet(balances)
    offerconfig = (txfee, cjfee_a, cjfee_r, ordertype, minsize)

    yg = YieldGeneratorBasic(WalletService(wallet), offerconfig)

    # We don't need any of the logic from Maker, including the waiting
    # loop.  Just stop it, so that it does not linger around and create
    # unclean-reactor failures.
    if yg.sync_wait_loop.running:
        yg.sync_wait_loop.stop()

    return yg
示例#12
0
def test_address_reuse_freezing(setup_walletservice):
    """ Creates a WalletService on a pre-populated wallet,
    and sets different values of the config var
    'max_sats_freeze_reuse' then adds utxos to different
    already used addresses to check that they are frozen or
    not as appropriate.
    Note that to avoid a twisted main loop the WalletService is
    not actually started, but the transaction_monitor is triggered
    manually (which executes the same code).
    A custom test version of the reuse warning callback is added
    and to check correct function, we check that this callback is
    called, and that the balance available in the mixdepth correctly
    reflects the usage pattern and freeze policy.
    """
    cb_called = 0

    def reuse_callback(utxostr):
        nonlocal cb_called
        print("Address reuse freezing callback on utxo: ", utxostr)
        cb_called += 1

    # we must fund after initial sync (for imports), hence
    # "populated" with no coins
    wallet = get_populated_wallet(num=0)
    wallet_service = WalletService(wallet)
    wallet_service.set_autofreeze_warning_cb(reuse_callback)
    sync_test_wallet(True, wallet_service)
    for i in range(3):
        fund_wallet_addr(wallet_service.wallet,
                         wallet_service.get_addr(0, 1, i))
    # manually force the wallet service to see the new utxos:
    wallet_service.transaction_monitor()

    # check that with default status any reuse is blocked:
    try_address_reuse(wallet_service, 0, 1, -1, 3 * 10**8)
    assert cb_called == 1, "Failed to trigger freeze callback"

    # check that above the threshold is allowed (1 sat less than funding)
    try_address_reuse(wallet_service, 1, 1, 99999999, 4 * 10**8)
    assert cb_called == 1, "Incorrectly triggered freeze callback"

    # check that below the threshold on the same address is not allowed:
    try_address_reuse(wallet_service, 1, 0.99999998, 99999999, 4 * 10**8)
    # note can be more than 1 extra call here, somewhat suboptimal:
    assert cb_called > 1, "Failed to trigger freeze callback"
示例#13
0
def main():
    tumble_log = get_tumble_log(logsdir)
    (options, args) = get_tumbler_parser().parse_args()
    options_org = options
    options = vars(options)
    if len(args) < 1:
        jmprint('Error: Needs a wallet file', "error")
        sys.exit(EXIT_ARGERROR)
    load_program_config(config_path=options['datadir'])

    if jm_single().bc_interface is None:
        jmprint('Error: Needs a blockchain source', "error")
        sys.exit(EXIT_FAILURE)

    check_regtest()

    #Load the wallet
    wallet_name = args[0]
    max_mix_depth = options['mixdepthsrc'] + options['mixdepthcount']
    if options['amtmixdepths'] > max_mix_depth:
        max_mix_depth = options['amtmixdepths']
    wallet_path = get_wallet_path(wallet_name, None)
    wallet = open_test_wallet_maybe(
        wallet_path,
        wallet_name,
        max_mix_depth,
        wallet_password_stdin=options_org.wallet_password_stdin)
    wallet_service = WalletService(wallet)
    if wallet_service.rpc_error:
        sys.exit(EXIT_FAILURE)
    # in this script, we need the wallet synced before
    # logic processing for some paths, so do it now:
    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=not options['recoversync'])
    # the sync call here will now be a no-op:
    wallet_service.startService()

    maxcjfee = get_max_cj_fee_values(jm_single().config, options_org)
    log.info(
        "Using maximum coinjoin fee limits per maker of {:.4%}, {} sat".format(
            *maxcjfee))

    #Parse options and generate schedule
    #Output information to log files
    jm_single().mincjamount = options['mincjamount']
    destaddrs = args[1:]
    for daddr in destaddrs:
        success, errmsg = validate_address(daddr)
        if not success:
            jmprint("Invalid destination address: " + daddr, "error")
            sys.exit(EXIT_ARGERROR)
    jmprint("Destination addresses: " + str(destaddrs), "important")
    #If the --restart flag is set we read the schedule
    #from the file, and filter out entries that are
    #already complete
    if options['restart']:
        res, schedule = get_schedule(
            os.path.join(logsdir, options['schedulefile']))
        if not res:
            jmprint(
                "Failed to load schedule, name: " +
                str(options['schedulefile']), "error")
            jmprint("Error was: " + str(schedule), "error")
            sys.exit(EXIT_FAILURE)
        #This removes all entries that are marked as done
        schedule = [s for s in schedule if s[-1] != 1]
        # remaining destination addresses must be stored in Taker.tdestaddrs
        # in case of tweaks; note we can't change, so any passed on command
        # line must be ignored:
        if len(destaddrs) > 0:
            jmprint(
                "For restarts, destinations are taken from schedule file,"
                " so passed destinations on the command line were ignored.",
                "important")
            if input("OK? (y/n)") != "y":
                sys.exit(EXIT_SUCCESS)
        destaddrs = [
            s[3] for s in schedule if s[3] not in ["INTERNAL", "addrask"]
        ]
        jmprint(
            "Remaining destination addresses in restart: " +
            ",".join(destaddrs), "important")
        if isinstance(schedule[0][-1], str) and len(schedule[0][-1]) == 64:
            #ensure last transaction is confirmed before restart
            tumble_log.info("WAITING TO RESTART...")
            txid = schedule[0][-1]
            restart_waiter(txid)
            #remove the already-done entry (this connects to the other TODO,
            #probably better *not* to truncate the done-already txs from file,
            #but simplest for now.
            schedule = schedule[1:]
        elif schedule[0][-1] != 0:
            print("Error: first schedule entry is invalid.")
            sys.exit(EXIT_FAILURE)
        with open(os.path.join(logsdir, options['schedulefile']), "wb") as f:
            f.write(schedule_to_text(schedule))
        tumble_log.info("TUMBLE RESTARTING")
    else:
        #Create a new schedule from scratch
        schedule = get_tumble_schedule(options, destaddrs,
                                       wallet.get_balance_by_mixdepth())
        tumble_log.info("TUMBLE STARTING")
        with open(os.path.join(logsdir, options['schedulefile']), "wb") as f:
            f.write(schedule_to_text(schedule))
        print("Schedule written to logs/" + options['schedulefile'])
    tumble_log.info("With this schedule: ")
    tumble_log.info(pprint.pformat(schedule))

    # If tx_fees are set manually by CLI argument, override joinmarket.cfg:
    if int(options['txfee']) > 0:
        jm_single().config.set("POLICY", "tx_fees", str(options['txfee']))

    # Dynamically estimate an expected tx fee for the whole tumbling run.
    # This is very rough: we guess with 2 inputs and 2 outputs each.
    fee_per_cp_guess = estimate_tx_fee(2,
                                       2,
                                       txtype=wallet_service.get_txtype())
    log.debug("Estimated miner/tx fee for each cj participant: " +
              str(fee_per_cp_guess))

    # From the estimated tx fees, check if the expected amount is a
    # significant value compared the the cj amount
    involved_parties = len(schedule)  # own participation in each CJ
    for item in schedule:
        involved_parties += item[2]  #  number of total tumble counterparties
    total_tumble_amount = int(0)
    max_mix_to_tumble = min(options['mixdepthsrc']+options['mixdepthcount'], \
                            max_mix_depth)
    for i in range(options['mixdepthsrc'], max_mix_to_tumble):
        total_tumble_amount += wallet_service.get_balance_by_mixdepth()[i]
        if total_tumble_amount == 0:
            raise ValueError(
                "No confirmed coins in the selected mixdepth(s). Quitting")
    exp_tx_fees_ratio = (involved_parties * fee_per_cp_guess) \
        / total_tumble_amount
    if exp_tx_fees_ratio > 0.05:
        jmprint(
            'WARNING: Expected bitcoin network miner fees for the whole '
            'tumbling run are roughly {:.1%}'.format(exp_tx_fees_ratio),
            "warning")
        if not options['restart'] and input(
                'You might want to modify your tx_fee'
                ' settings in joinmarket.cfg. Still continue? (y/n):'
        )[0] != 'y':
            sys.exit('Aborted by user.')
    else:
        log.info("Estimated miner/tx fees for this coinjoin amount for the "
                 "whole tumbling run: {:.1%}".format(exp_tx_fees_ratio))

    print("Progress logging to logs/TUMBLE.log")

    def filter_orders_callback(orders_fees, cjamount):
        """Decide whether to accept fees
        """
        return tumbler_filter_orders_callback(orders_fees, cjamount, taker)

    def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None):
        """on_finished_callback for tumbler; processing is almost entirely
        deferred to generic taker_finished in tumbler_support module, except
        here reactor signalling.
        """
        sfile = os.path.join(logsdir, options['schedulefile'])
        tumbler_taker_finished_update(taker, sfile, tumble_log, options, res,
                                      fromtx, waittime, txdetails)
        if not fromtx:
            reactor.stop()
        elif fromtx != "unconfirmed":
            reactor.callLater(waittime * 60,
                              clientfactory.getClient().clientStart)

    #instantiate Taker with given schedule and run
    taker = Taker(wallet_service,
                  schedule,
                  maxcjfee,
                  order_chooser=options['order_choose_fn'],
                  callbacks=(filter_orders_callback, None, taker_finished),
                  tdestaddrs=destaddrs)
    clientfactory = JMClientProtocolFactory(taker)
    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 else False
    if jm_single().config.get("BLOCKCHAIN",
                              "network") in ["regtest", "testnet"]:
        startLogging(sys.stdout)
    start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                  jm_single().config.getint("DAEMON", "daemon_port"),
                  clientfactory,
                  daemon=daemon)
def ygmain(ygclass, nickserv_password='', gaplimit=6):
    import sys

    parser = OptionParser(usage='usage: %prog [options] [wallet file]')
    add_base_options(parser)
    # A note about defaults:
    # We want command line settings to override config settings.
    # This would naturally mean setting `default=` arguments here, to the
    # values in the config.
    # However, we cannot load the config until we know the datadir.
    # The datadir is a setting in the command line options, so we have to
    # call parser.parse_args() before we know the datadir.
    # Hence we do the following: set all modifyable-by-config arguments to
    # default "None" initially; call parse_args(); then call load_program_config
    # and override values of "None" with what is set in the config.
    # (remember, the joinmarket defaultconfig always sets every value, even if
    # the user doesn't).
    parser.add_option('-o',
                      '--ordertype',
                      action='store',
                      type='string',
                      dest='ordertype',
                      default=None,
                      help='type of order; can be either reloffer or absoffer')
    parser.add_option('-t',
                      '--txfee',
                      action='store',
                      type='int',
                      dest='txfee',
                      default=None,
                      help='minimum miner fee in satoshis')
    parser.add_option('-f',
                      '--txfee-factor',
                      action='store',
                      type='float',
                      dest='txfee_factor',
                      default=None,
                      help='variance around the average fee, decimal fraction')
    parser.add_option('-a',
                      '--cjfee-a',
                      action='store',
                      type='string',
                      dest='cjfee_a',
                      default=None,
                      help='requested coinjoin fee (absolute) in satoshis')
    parser.add_option('-r',
                      '--cjfee-r',
                      action='store',
                      type='string',
                      dest='cjfee_r',
                      default=None,
                      help='requested coinjoin fee (relative) as a decimal')
    parser.add_option('-j',
                      '--cjfee-factor',
                      action='store',
                      type='float',
                      dest='cjfee_factor',
                      default=None,
                      help='variance around the average fee, decimal fraction')
    parser.add_option('-p',
                      '--password',
                      action='store',
                      type='string',
                      dest='password',
                      default=nickserv_password,
                      help='irc nickserv password')
    parser.add_option('-s',
                      '--minsize',
                      action='store',
                      type='int',
                      dest='minsize',
                      default=None,
                      help='minimum coinjoin size in satoshis')
    parser.add_option('-z',
                      '--size-factor',
                      action='store',
                      type='float',
                      dest='size_factor',
                      default=None,
                      help='variance around all offer sizes, decimal fraction')
    parser.add_option('-g',
                      '--gap-limit',
                      action='store',
                      type="int",
                      dest='gaplimit',
                      default=gaplimit,
                      help='gap limit for wallet, default=' + str(gaplimit))
    parser.add_option('-m',
                      '--mixdepth',
                      action='store',
                      type='int',
                      dest='mixdepth',
                      default=None,
                      help="highest mixdepth to use")
    (options, args) = parser.parse_args()
    # for string access, convert to dict:
    options = vars(options)
    if len(args) < 1:
        parser.error('Needs a wallet')
        sys.exit(EXIT_ARGERROR)

    load_program_config(config_path=options["datadir"])

    # As per previous note, override non-default command line settings:
    for x in [
            "ordertype", "txfee", "txfee_factor", "cjfee_a", "cjfee_r",
            "cjfee_factor", "minsize", "size_factor"
    ]:
        if options[x] is None:
            options[x] = jm_single().config.get("YIELDGENERATOR", x)
    wallet_name = args[0]
    ordertype = options["ordertype"]
    txfee = int(options["txfee"])
    txfee_factor = float(options["txfee_factor"])
    cjfee_factor = float(options["cjfee_factor"])
    size_factor = float(options["size_factor"])
    if ordertype == 'reloffer':
        cjfee_r = options["cjfee_r"]
        # minimum size is such that you always net profit at least 20%
        #of the miner fee
        minsize = max(int(1.2 * txfee / float(cjfee_r)),
                      int(options["minsize"]))
        cjfee_a = None
    elif ordertype == 'absoffer':
        cjfee_a = int(options["cjfee_a"])
        minsize = int(options["minsize"])
        cjfee_r = None
    else:
        parser.error('You specified an incorrect offer type which ' +\
                     'can be either reloffer or absoffer')
        sys.exit(EXIT_ARGERROR)
    nickserv_password = options["password"]

    if jm_single().bc_interface is None:
        jlog.error("Running yield generator requires configured " +
                   "blockchain source.")
        sys.exit(EXIT_FAILURE)

    wallet_path = get_wallet_path(wallet_name, None)
    wallet = open_test_wallet_maybe(
        wallet_path,
        wallet_name,
        options["mixdepth"],
        wallet_password_stdin=options["wallet_password_stdin"],
        gap_limit=options["gaplimit"])

    wallet_service = WalletService(wallet)
    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=not options["recoversync"])
    wallet_service.startService()

    txtype = wallet_service.get_txtype()
    if txtype == "p2wpkh":
        prefix = "sw0"
    elif txtype == "p2sh-p2wpkh":
        prefix = "sw"
    elif txtype == "p2pkh":
        prefix = ""
    else:
        jlog.error("Unsupported wallet type for yieldgenerator: " + txtype)
        sys.exit(EXIT_ARGERROR)

    ordertype = prefix + ordertype
    jlog.debug("Set the offer type string to: " + ordertype)

    maker = ygclass(wallet_service, [
        txfee, cjfee_a, cjfee_r, ordertype, minsize, txfee_factor,
        cjfee_factor, size_factor
    ])
    jlog.info('starting yield generator')
    clientfactory = JMClientProtocolFactory(maker, proto_type="MAKER")
    if jm_single().config.get("SNICKER", "enabled") == "true":
        if jm_single().config.get("BLOCKCHAIN", "network") == "mainnet":
            jlog.error("You have enabled SNICKER on mainnet, this is not "
                       "yet supported for yieldgenerators; either use "
                       "signet/regtest/testnet, or run SNICKER manually "
                       "with snicker/receive-snicker.py.")
            sys.exit(EXIT_ARGERROR)
        snicker_r = SNICKERReceiver(wallet_service)
        servers = jm_single().config.get("SNICKER", "servers").split(",")
        snicker_factory = SNICKERClientProtocolFactory(snicker_r, servers)
    else:
        snicker_factory = None
    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 else False
    if jm_single().config.get("BLOCKCHAIN",
                              "network") in ["regtest", "testnet", "signet"]:
        startLogging(sys.stdout)
    start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                  jm_single().config.getint("DAEMON", "daemon_port"),
                  clientfactory,
                  snickerfactory=snicker_factory,
                  daemon=daemon)
示例#15
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]')
    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)
示例#16
0
def main():
    parser = OptionParser(usage='usage: %prog [options] walletname',
                          description=description)
    parser.add_option('-m',
                      '--mixdepth',
                      action='store',
                      type='int',
                      dest='mixdepth',
                      default=0,
                      help="mixdepth to source coins from")
    parser.add_option('-a',
                      '--amtmixdepths',
                      action='store',
                      type='int',
                      dest='amtmixdepths',
                      help='number of mixdepths in wallet, default 5',
                      default=5)
    parser.add_option('-g',
                      '--gap-limit',
                      type="int",
                      action='store',
                      dest='gaplimit',
                      help='gap limit for wallet, default=6',
                      default=6)
    add_base_options(parser)
    (options, args) = parser.parse_args()
    load_program_config(config_path=options.datadir)
    check_regtest()
    if len(args) != 1:
        log.error("Invalid arguments, see --help")
        sys.exit(EXIT_ARGERROR)
    wallet_name = args[0]
    wallet_path = get_wallet_path(wallet_name, None)
    max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1])
    wallet = open_test_wallet_maybe(
        wallet_path,
        wallet_name,
        max_mix_depth,
        wallet_password_stdin=options.wallet_password_stdin,
        gap_limit=options.gaplimit)
    wallet_service = WalletService(wallet)

    # step 1: do a full recovery style sync. this will pick up
    # all addresses that we expect to match transactions against,
    # from a blank slate Core wallet that originally had no imports.
    if not options.recoversync:
        jmprint("Recovery sync was not set, but using it anyway.")
    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=False)
    # Note that the user may be interrupted above by the rescan
    # request; this is as for normal scripts; after the rescan is done
    # (usually, only once, but, this *IS* needed here, unlike a normal
    # wallet generation event), we just try again.

    # Now all address from HD are imported, we need to grab
    # all the transactions for those addresses; this includes txs
    # that *spend* as well as receive our coins, so will include
    # "first-out" SNICKER txs as well as ordinary spends and JM coinjoins.
    seed_transactions = wallet_service.get_all_transactions()

    # Search for SNICKER txs and add them if they match.
    # We proceed recursively; we find all one-out matches, then
    # all 2-out matches, until we find no new ones and stop.

    if len(seed_transactions) == 0:
        jmprint("No transactions were found for this wallet. Did you rescan?")
        return False

    new_txs = []
    current_block_heights = set()
    for tx in seed_transactions:
        if btc.is_snicker_tx(tx):
            jmprint("Found a snicker tx: {}".format(
                bintohex(tx.GetTxid()[::-1])))
            equal_outs = btc.get_equal_outs(tx)
            if not equal_outs:
                continue
            if all([
                    wallet_service.is_known_script(x.scriptPubKey) == False
                    for x in [a[1] for a in equal_outs]
            ]):
                # it is now *very* likely that one of the two equal
                # outputs is our SNICKER custom output
                # script; notice that in this case, the transaction *must*
                # have spent our inputs, since it didn't recognize ownership
                # of either coinjoin output (and if it did recognize the change,
                # it would have recognized the cj output also).
                # We try to regenerate one of the outputs, but warn if
                # we can't.
                my_indices = get_pubs_and_indices_of_inputs(tx,
                                                            wallet_service,
                                                            ours=True)
                for mypub, mi in my_indices:
                    for eo in equal_outs:
                        for (other_pub, i) in get_pubs_and_indices_of_inputs(
                                tx, wallet_service, ours=False):
                            for (our_pub,
                                 j) in get_pubs_and_indices_of_ancestor_inputs(
                                     tx.vin[mi], wallet_service, ours=True):
                                our_spk = wallet_service.pubkey_to_script(
                                    our_pub)
                                our_priv = wallet_service.get_key_from_addr(
                                    wallet_service.script_to_addr(our_spk))
                                tweak_bytes = btc.ecdh(our_priv[:-1],
                                                       other_pub)
                                tweaked_pub = btc.snicker_pubkey_tweak(
                                    our_pub, tweak_bytes)
                                tweaked_spk = wallet_service.pubkey_to_script(
                                    tweaked_pub)
                                if tweaked_spk == eo[1].scriptPubKey:
                                    # TODO wallet.script_to_addr has a dubious assertion, that's why
                                    # we use btc method directly:
                                    address_found = str(
                                        btc.CCoinAddress.from_scriptPubKey(
                                            btc.CScript(tweaked_spk)))
                                    #address_found = wallet_service.script_to_addr(tweaked_spk)
                                    jmprint(
                                        "Found a new SNICKER output belonging to us."
                                    )
                                    jmprint(
                                        "Output address {} in the following transaction:"
                                        .format(address_found))
                                    jmprint(btc.human_readable_transaction(tx))
                                    jmprint(
                                        "Importing the address into the joinmarket wallet..."
                                    )
                                    # NB for a recovery we accept putting any imported keys all into
                                    # the same mixdepth (0); TODO investigate correcting this, it will
                                    # be a little complicated.
                                    success, msg = wallet_service.check_tweak_matches_and_import(
                                        wallet_service.script_to_addr(our_spk),
                                        tweak_bytes, tweaked_pub,
                                        wallet_service.mixdepth)
                                    if not success:
                                        jmprint(
                                            "Failed to import SNICKER key: {}".
                                            format(msg), "error")
                                        return False
                                    else:
                                        jmprint("... success.")
                                    # we want the blockheight to track where the next-round rescan
                                    # must start from
                                    current_block_heights.add(
                                        wallet_service.
                                        get_transaction_block_height(tx))
                                    # add this transaction to the next round.
                                    new_txs.append(tx)
    if len(new_txs) == 0:
        return True
    seed_transactions.extend(new_txs)
    earliest_new_blockheight = min(current_block_heights)
    jmprint("New SNICKER addresses were imported to the Core wallet; "
            "do rescanblockchain again, starting from block {}, before "
            "restarting this script.".format(earliest_new_blockheight))
    return False
    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
    else:
        walletclass = SegwitLegacyWallet
    wallet = open_test_wallet_maybe(wallet_path,
                                    wallet_name,
                                    4,
                                    wallet_password_stdin=False,
                                    test_wallet_cls=walletclass,
                                    gap_limit=6)
    wallet_service = WalletService(wallet)
    # in this script, we need the wallet synced before
    # logic processing for some paths, so do it now:
    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=True)
    # the sync call here will now be a no-op:
    wallet_service.startService()
    manager = parse_payjoin_setup(bip21uri, wallet_service, mixdepth)
    if usessl == 0:
        tlshostnames = None
    else:
        tlshostnames = [b"127.0.0.1"]
    reactor.callWhenRunning(send_payjoin, manager, tls_whitelist=tlshostnames)
    reactor.run()
def main():
    parser = OptionParser(usage='usage: %prog [options] walletname',
                          description=description)
    add_base_options(parser)
    parser.add_option('-m',
                      '--mixdepth',
                      action='store',
                      type='int',
                      dest='mixdepth',
                      help='mixdepth/account, default 0',
                      default=0)
    parser.add_option('-g',
                      '--gap-limit',
                      action='store',
                      type='int',
                      dest='gaplimit',
                      default=6,
                      help='gap limit for Joinmarket wallet, default 6.')
    parser.add_option(
        '-f',
        '--txfee',
        action='store',
        type='int',
        dest='txfee',
        default=-1,
        help='Bitcoin miner tx_fee to use for transaction(s). A number higher '
        'than 1000 is used as "satoshi per KB" tx fee. A number lower than that '
        'uses the dynamic fee estimation of your blockchain provider as '
        'confirmation target. This temporarily overrides the "tx_fees" setting '
        'in your joinmarket.cfg. Works the same way as described in it. Check '
        'it for examples.')
    parser.add_option('-a',
                      '--amtmixdepths',
                      action='store',
                      type='int',
                      dest='amtmixdepths',
                      help='number of mixdepths in wallet, default 5',
                      default=5)
    parser.add_option(
        '-N',
        '--net-transfer',
        action='store',
        type='int',
        dest='net_transfer',
        help='how many sats are sent to the "receiver", default randomised.',
        default=-1000001)
    (options, args) = parser.parse_args()
    snicker_plugin = JMPluginService("SNICKER")
    load_program_config(config_path=options.datadir,
                        plugin_services=[snicker_plugin])
    if len(args) != 1:
        log.error("Invalid arguments, see --help")
        sys.exit(EXIT_ARGERROR)
    wallet_name = args[0]
    check_regtest()
    # 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))
    max_mix_depth = max([options.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)
    snicker_plugin.start_plugin_logging(wallet_service)
    # 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()
    fee_est = estimate_tx_fee(2, 3, txtype=wallet_service.get_txtype())

    # first, order the utxos in the mixepth by size. Then (this is the
    # simplest algorithm; we could be more sophisticated), choose the
    # *second* largest utxo as the receiver utxo; this ensures that we
    # have enough for the proposer to cover. We consume utxos greedily,
    # meaning we'll at least some of the time, be consolidating.
    utxo_dict = wallet_service.get_utxos_by_mixdepth()[options.mixdepth]
    if not len(utxo_dict) >= 2:
        log.error(
            "Cannot create fake SNICKER tx without at least two utxos, quitting"
        )
        sys.exit(EXIT_ARGERROR)
    # sort utxos by size
    sorted_utxos = sorted(list(utxo_dict.keys()),
                          key=lambda k: utxo_dict[k]['value'],
                          reverse=True)
    # receiver is the second largest:
    receiver_utxo = sorted_utxos[1]
    receiver_utxo_val = utxo_dict[receiver_utxo]
    # gather the other utxos into a list to select from:
    nonreceiver_utxos = [sorted_utxos[0]] + sorted_utxos[2:]
    # get the net transfer in our fake coinjoin:
    if options.net_transfer < -1000001:
        log.error("Net transfer must be greater than negative 1M sats")
        sys.exit(EXIT_ARGERROR)
    if options.net_transfer == -1000001:
        # default; low-ish is more realistic and avoids problems
        # with dusty utxos
        options.net_transfer = random.randint(-1000, 1000)

    # select enough to cover: receiver value + fee + transfer + breathing room
    # we select relatively greedily to support consolidation, since
    # this transaction does not pretend to isolate the coins.
    try:
        available = [{
            'utxo': utxo,
            'value': utxo_dict[utxo]["value"]
        } for utxo in nonreceiver_utxos]
        # selection algos return [{"utxo":..,"value":..}]:
        prop_utxos = {
            x["utxo"]
            for x in select_greedy(
                available, receiver_utxo_val["value"] + fee_est +
                options.net_transfer + 1000)
        }
        prop_utxos = list(prop_utxos)
        prop_utxo_vals = [utxo_dict[prop_utxo] for prop_utxo in prop_utxos]
    except NotEnoughFundsException as e:
        log.error(repr(e))
        sys.exit(EXIT_FAILURE)

    # Due to the fake nature of this transaction, and its distinguishability
    # (not only in trivial output pattern, but also in subset-sum), there
    # is little advantage in making it use different output mixdepths, so
    # here to prevent fragmentation, everything is kept in the same mixdepth.
    receiver_addr, proposer_addr, change_addr = (wallet_service.script_to_addr(
        wallet_service.get_new_script(options.mixdepth, 1)) for _ in range(3))
    # persist index update:
    wallet_service.save_wallet()
    outputs = btc.construct_snicker_outputs(
        sum([x["value"] for x in prop_utxo_vals]), receiver_utxo_val["value"],
        receiver_addr, proposer_addr, change_addr, fee_est,
        options.net_transfer)
    tx = btc.make_shuffled_tx(prop_utxos + [receiver_utxo],
                              outputs,
                              version=2,
                              locktime=0)
    # before signing, check we satisfied the criteria, otherwise
    # this is pointless!
    if not btc.is_snicker_tx(tx):
        log.error("Code error, created non-SNICKER tx, not signing.")
        sys.exit(EXIT_FAILURE)

    # sign all inputs
    # scripts: {input_index: (output_script, amount)}
    our_inputs = {}
    for index, ins in enumerate(tx.vin):
        utxo = (ins.prevout.hash[::-1], ins.prevout.n)
        script = utxo_dict[utxo]['script']
        amount = utxo_dict[utxo]['value']
        our_inputs[index] = (script, amount)
    success, msg = wallet_service.sign_tx(tx, our_inputs)
    if not success:
        log.error("Failed to sign transaction: " + msg)
        sys.exit(EXIT_FAILURE)
    # TODO condition on automatic brdcst or not
    if not jm_single().bc_interface.pushtx(tx.serialize()):
        # this represents an error about state (or conceivably,
        # an ultra-short window in which the spent utxo was
        # consumed in another transaction), but not really
        # an internal logic error, so we do NOT return False
        log.error("Failed to broadcast fake SNICKER coinjoin: " +\
                   bintohex(tx.GetTxid()[::-1]))
        log.info(btc.human_readable_transaction(tx))
        sys.exit(EXIT_FAILURE)
    log.info("Successfully broadcast fake SNICKER coinjoin: " +\
              bintohex(tx.GetTxid()[::-1]))
示例#19
0
def receive_payjoin_main(makerclass):
    parser = OptionParser(
        usage='usage: %prog [options] [wallet file] [amount-to-receive]')
    parser.add_option('-g',
                      '--gap-limit',
                      action='store',
                      type="int",
                      dest='gaplimit',
                      default=6,
                      help='gap limit for wallet, default=6')
    parser.add_option('--recoversync',
                      action='store_true',
                      dest='recoversync',
                      default=False,
                      help=('choose to do detailed wallet sync, '
                            'used for recovering on new Core instance.'))
    parser.add_option('-m',
                      '--mixdepth',
                      action='store',
                      type='int',
                      dest='mixdepth',
                      default=0,
                      help="mixdepth to source coins from")
    parser.add_option('-a',
                      '--amtmixdepths',
                      action='store',
                      type='int',
                      dest='amtmixdepths',
                      help='number of mixdepths in wallet, default 5',
                      default=5)
    parser.add_option('--wallet-password-stdin',
                      action='store_true',
                      default=False,
                      dest='wallet_password_stdin',
                      help='Read wallet password from stdin')

    (options, args) = parser.parse_args()
    if len(args) < 2:
        parser.error(
            'Needs a wallet, and a receiving amount in bitcoins or satoshis')
        sys.exit(EXIT_ARGERROR)
    wallet_name = args[0]
    try:
        receiving_amount = amount_to_sat(args[1])
    except:
        parser.error("Invalid receiving amount passed: " + receiving_amount)
        sys.exit(EXIT_FAILURE)
    if receiving_amount < 0:
        parser.error("Receiving amount must be a positive number")
        sys.exit(EXIT_FAILURE)
    load_program_config()

    check_regtest()

    # This workflow requires command line reading; we force info level logging
    # to remove noise, and mostly communicate to the user with the fn
    # log.info (via P2EPMaker.user_info).
    set_logging_level("INFO")

    wallet_path = get_wallet_path(wallet_name, 'wallets')
    max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1])
    wallet = open_test_wallet_maybe(
        wallet_path,
        wallet_name,
        max_mix_depth,
        wallet_password_stdin=options.wallet_password_stdin,
        gap_limit=options.gaplimit)
    wallet_service = WalletService(wallet)

    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=not options.recoversync)
    wallet_service.startService()

    maker = makerclass(wallet_service, options.mixdepth, receiving_amount)

    jlog.info('starting receive-payjoin')
    clientfactory = JMClientProtocolFactory(maker, proto_type="MAKER")

    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 else False
    if jm_single().config.get("BLOCKCHAIN",
                              "network") in ["regtest", "testnet"]:
        startLogging(sys.stdout)
    start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                  jm_single().config.getint("DAEMON", "daemon_port"),
                  clientfactory,
                  daemon=daemon,
                  p2ep=True)
def main():
    parser = 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 p2wpkh (native segwit) "
        "or p2sh-p2wpkh (nested segwit) utxos.")
    add_base_options(parser)
    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)
    (options, args) = parser.parse_args()
    load_program_config(config_path=options.datadir)
    #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 input("You have chosen to delete commitments, other arguments "
                     "will be ignored; continue? (y/n)") != 'y':
                jmprint("Quitting", "warning")
                sys.exit(EXIT_SUCCESS)
        c, e = get_podle_commitments()
        jmprint(pformat(e), "info")
        if input("You will remove the above commitments; are you sure? (y/n): "
                 ) != 'y':
            jmprint("Quitting", "warning")
            sys.exit(EXIT_SUCCESS)
        update_commitments(external_to_remove=e)
        jmprint("Commitments deleted.", "important")
        sys.exit(EXIT_SUCCESS)

    #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)
        wallet = open_wallet(wallet_path, gap_limit=options.gaplimit)
        wallet_service = WalletService(wallet)
        if wallet_service.rpc_error:
            sys.exit(EXIT_FAILURE)
        while True:
            if wallet_service.sync_wallet(fast=not options.recoversync):
                break

        # minor note: adding a utxo from an external wallet for commitments, we
        # default to not allowing disabled utxos to avoid a privacy leak, so the
        # user would have to explicitly enable.
        for md, utxos in wallet_service.get_utxos_by_mixdepth().items():
            for utxo, utxodata in utxos.items():
                wif = wallet_service.get_wif_path(utxodata['path'])
                utxo_data.append((utxo, 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.decode("utf-8"), utxo_binary=True)
                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):
            jmprint("File: " + options.in_json + " not found.", "error")
            sys.exit(EXIT_FAILURE)
        with open(options.in_json, "rb") as f:
            try:
                utxo_json = json.loads(f.read())
            except:
                jmprint("Failed to read json from " + options.in_json, "error")
                sys.exit(EXIT_FAILURE)
        for u, pva in iteritems(utxo_json):
            utxobin, priv = get_utxo_info(",".join([u, pva["privkey"]]),
                                          utxo_binary=True)
            if not utxobin:
                quit(parser, "Failed to load utxo from json: " + str(u))
            utxo_data.append((utxobin, priv))
    elif len(args) == 1:
        ul = args[0]
        priv = input('input private key for ' + ul +
                     ', in WIF compressed format : ')
        u, priv = get_utxo_info(','.join([ul, priv]), utxo_binary=True)
        if not u:
            quit(parser, "Failed to parse utxo info: " + ul)
        utxo_data.append((u, priv))
    else:
        quit(parser, 'Invalid syntax')
    if options.validate or options.vonly:
        # if the utxos are loaded from a wallet, we use the wallet's
        # txtype to determine the value of `utxo_address_type`; if not,
        # we use joinmarket.cfg.
        if options.loadwallet:
            utxo_address_type = wallet_service.get_txtype()
        else:
            if jm_single().config.get("POLICY", "segwit") == "false":
                utxo_address_type = "p2pkh"
            elif jm_single().config.get("POLICY", "native") == "false":
                utxo_address_type = "p2sh-p2wpkh"
            else:
                utxo_address_type = "p2wpkh"
        if not validate_utxo_data(utxo_data,
                                  utxo_address_type=utxo_address_type):
            quit(parser, "Utxos did not validate, quitting")
    if options.vonly:
        sys.exit(EXIT_SUCCESS)

    #We are adding utxos to the external list
    assert len(utxo_data)
    add_ext_commitments(utxo_data)
def main():
    parser = get_sendpayment_parser()
    (options, args) = parser.parse_args()
    load_program_config(config_path=options.datadir)
    if options.p2ep and len(args) != 3:
        parser.error("PayJoin requires exactly three arguments: "
                     "wallet, amount and destination address.")
        sys.exit(EXIT_ARGERROR)
    elif options.schedule == '' and len(args) != 3:
        parser.error("Joinmarket sendpayment (coinjoin) needs arguments:"
                     " wallet, amount and destination address")
        sys.exit(EXIT_ARGERROR)

    #without schedule file option, use the arguments to create a schedule
    #of a single transaction
    sweeping = False
    if options.schedule == '':
        amount = btc.amount_to_sat(args[1])
        if amount == 0:
            sweeping = True
        destaddr = args[2]
        mixdepth = options.mixdepth
        addr_valid, errormsg = validate_address(destaddr)
        if not addr_valid:
            jmprint('ERROR: Address invalid. ' + errormsg, "error")
            sys.exit(EXIT_ARGERROR)
        if sweeping == False and amount < DUST_THRESHOLD:
            jmprint(
                'ERROR: Amount ' + btc.amount_to_str(amount) +
                ' is below dust threshold ' +
                btc.amount_to_str(DUST_THRESHOLD) + '.', "error")
            sys.exit(EXIT_ARGERROR)
        if (options.makercount != 0
                and options.makercount < jm_single().config.getint(
                    "POLICY", "minimum_makers")):
            jmprint(
                'ERROR: Maker count ' + str(options.makercount) +
                ' below minimum_makers (' +
                str(jm_single().config.getint("POLICY", "minimum_makers")) +
                ') in joinmarket.cfg.', "error")
            sys.exit(EXIT_ARGERROR)
        schedule = [[
            options.mixdepth, amount, options.makercount, destaddr, 0.0,
            NO_ROUNDING, 0
        ]]
    else:
        if options.p2ep:
            parser.error("Schedule files are not compatible with PayJoin")
            sys.exit(EXIT_FAILURE)
        result, schedule = get_schedule(options.schedule)
        if not result:
            log.error(
                "Failed to load schedule file, quitting. Check the syntax.")
            log.error("Error was: " + str(schedule))
            sys.exit(EXIT_FAILURE)
        mixdepth = 0
        for s in schedule:
            if s[1] == 0:
                sweeping = True
            #only used for checking the maximum mixdepth required
            mixdepth = max([mixdepth, s[0]])

    wallet_name = args[0]

    check_regtest()

    if options.pickorders:
        chooseOrdersFunc = pick_order
        if sweeping:
            jmprint('WARNING: You may have to pick offers multiple times',
                    "warning")
            jmprint('WARNING: due to manual offer picking while sweeping',
                    "warning")
    else:
        chooseOrdersFunc = options.order_choose_fn

    # If tx_fees are set manually by CLI argument, override joinmarket.cfg:
    if int(options.txfee) > 0:
        jm_single().config.set("POLICY", "tx_fees", str(options.txfee))

    # Dynamically estimate a realistic fee if it currently is the default value.
    # At this point we do not know even the number of our own inputs, so
    # we guess conservatively with 2 inputs and 2 outputs each.
    if options.txfee == -1:
        options.txfee = max(options.txfee,
                            estimate_tx_fee(2, 2, txtype="p2sh-p2wpkh"))
        log.debug("Estimated miner/tx fee for each cj participant: " +
                  str(options.txfee))
    assert (options.txfee >= 0)

    maxcjfee = (1, float('inf'))
    if not options.p2ep and not options.pickorders and options.makercount != 0:
        maxcjfee = get_max_cj_fee_values(jm_single().config, options)
        log.info("Using maximum coinjoin fee limits per maker of {:.4%}, {} "
                 "".format(maxcjfee[0], btc.amount_to_str(maxcjfee[1])))

    log.debug('starting sendpayment')

    max_mix_depth = max([mixdepth, options.amtmixdepths - 1])

    wallet_path = get_wallet_path(wallet_name, None)
    wallet = open_test_wallet_maybe(
        wallet_path,
        wallet_name,
        max_mix_depth,
        wallet_password_stdin=options.wallet_password_stdin,
        gap_limit=options.gaplimit)
    wallet_service = WalletService(wallet)
    # in this script, we need the wallet synced before
    # logic processing for some paths, so do it now:
    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=not options.recoversync)
    # the sync call here will now be a no-op:
    wallet_service.startService()

    # From the estimated tx fees, check if the expected amount is a
    # significant value compared the the cj amount; currently enabled
    # only for single join (the predominant, non-advanced case)
    if options.schedule == '':
        total_cj_amount = amount
        if total_cj_amount == 0:
            total_cj_amount = wallet_service.get_balance_by_mixdepth()[
                options.mixdepth]
            if total_cj_amount == 0:
                raise ValueError(
                    "No confirmed coins in the selected mixdepth. Quitting")
        exp_tx_fees_ratio = (
            (1 + options.makercount) * options.txfee) / total_cj_amount
        if exp_tx_fees_ratio > 0.05:
            jmprint(
                'WARNING: Expected bitcoin network miner fees for this coinjoin'
                ' amount are roughly {:.1%}'.format(exp_tx_fees_ratio),
                "warning")
            if input('You might want to modify your tx_fee'
                     ' settings in joinmarket.cfg. Still continue? (y/n):'
                     )[0] != 'y':
                sys.exit('Aborted by user.')
        else:
            log.info(
                "Estimated miner/tx fees for this coinjoin amount: {:.1%}".
                format(exp_tx_fees_ratio))

    if options.makercount == 0 and not options.p2ep:
        direct_send(wallet_service, amount, mixdepth, destaddr,
                    options.answeryes)
        return

    if wallet.get_txtype() == 'p2pkh':
        jmprint(
            "Only direct sends (use -N 0) are supported for "
            "legacy (non-segwit) wallets.", "error")
        sys.exit(EXIT_ARGERROR)

    def filter_orders_callback(orders_fees, cjamount):
        orders, total_cj_fee = orders_fees
        log.info("Chose these orders: " + pprint.pformat(orders))
        log.info('total cj fee = ' + str(total_cj_fee))
        total_fee_pc = 1.0 * total_cj_fee / cjamount
        log.info('total coinjoin fee = ' +
                 str(float('%.3g' % (100.0 * total_fee_pc))) + '%')
        WARNING_THRESHOLD = 0.02  # 2%
        if total_fee_pc > WARNING_THRESHOLD:
            log.info('\n'.join(['=' * 60] * 3))
            log.info('WARNING   ' * 6)
            log.info('\n'.join(['=' * 60] * 1))
            log.info(
                'OFFERED COINJOIN FEE IS UNUSUALLY HIGH. DOUBLE/TRIPLE CHECK.')
            log.info('\n'.join(['=' * 60] * 1))
            log.info('WARNING   ' * 6)
            log.info('\n'.join(['=' * 60] * 3))
        if not options.answeryes:
            if input('send with these orders? (y/n):')[0] != 'y':
                return False
        return True

    def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None):
        if fromtx == "unconfirmed":
            #If final entry, stop *here*, don't wait for confirmation
            if taker.schedule_index + 1 == len(taker.schedule):
                reactor.stop()
            return
        if fromtx:
            if res:
                txd, txid = txdetails
                reactor.callLater(waittime * 60,
                                  clientfactory.getClient().clientStart)
            else:
                #a transaction failed; we'll try to repeat without the
                #troublemakers.
                #If this error condition is reached from Phase 1 processing,
                #and there are less than minimum_makers honest responses, we
                #just give up (note that in tumbler we tweak and retry, but
                #for sendpayment the user is "online" and so can manually
                #try again).
                #However if the error is in Phase 2 and we have minimum_makers
                #or more responses, we do try to restart with the honest set, here.
                if taker.latest_tx is None:
                    #can only happen with < minimum_makers; see above.
                    log.info("A transaction failed but there are insufficient "
                             "honest respondants to continue; giving up.")
                    reactor.stop()
                    return
                #This is Phase 2; do we have enough to try again?
                taker.add_honest_makers(
                    list(
                        set(taker.maker_utxo_data.keys()).symmetric_difference(
                            set(taker.nonrespondants))))
                if len(taker.honest_makers) < jm_single().config.getint(
                        "POLICY", "minimum_makers"):
                    log.info("Too few makers responded honestly; "
                             "giving up this attempt.")
                    reactor.stop()
                    return
                jmprint("We failed to complete the transaction. The following "
                      "makers responded honestly: " + str(taker.honest_makers) +\
                      ", so we will retry with them.", "warning")
                #Now we have to set the specific group we want to use, and hopefully
                #they will respond again as they showed honesty last time.
                #we must reset the number of counterparties, as well as fix who they
                #are; this is because the number is used to e.g. calculate fees.
                #cleanest way is to reset the number in the schedule before restart.
                taker.schedule[taker.schedule_index][2] = len(
                    taker.honest_makers)
                log.info("Retrying with: " +
                         str(taker.schedule[taker.schedule_index][2]) +
                         " counterparties.")
                #rewind to try again (index is incremented in Taker.initialize())
                taker.schedule_index -= 1
                taker.set_honest_only(True)
                reactor.callLater(5.0, clientfactory.getClient().clientStart)
        else:
            if not res:
                log.info("Did not complete successfully, shutting down")
            #Should usually be unreachable, unless conf received out of order;
            #because we should stop on 'unconfirmed' for last (see above)
            else:
                log.info("All transactions completed correctly")
            reactor.stop()

    if options.p2ep:
        # This workflow requires command line reading; we force info level logging
        # to remove noise, and mostly communicate to the user with the fn
        # log.info (directly or via default taker_info_callback).
        set_logging_level("INFO")

        # in the case where the payment just hangs for a long period, allow
        # it to fail gracefully with an information message; this is triggered
        # only by the stallMonitor, which gives up after 20*maker_timeout_sec:
        def p2ep_on_finished_callback(res,
                                      fromtx=False,
                                      waittime=0.0,
                                      txdetails=None):
            log.error("PayJoin payment was NOT made, timed out.")
            reactor.stop()

        taker = P2EPTaker(options.p2ep,
                          wallet_service,
                          schedule,
                          callbacks=(None, None, p2ep_on_finished_callback))
    else:
        taker = Taker(wallet_service,
                      schedule,
                      order_chooser=chooseOrdersFunc,
                      max_cj_fee=maxcjfee,
                      callbacks=(filter_orders_callback, None, taker_finished))
    clientfactory = JMClientProtocolFactory(taker)
    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 else False
    p2ep = True if options.p2ep != "" else False
    if jm_single().config.get("BLOCKCHAIN",
                              "network") in ["regtest", "testnet"]:
        startLogging(sys.stdout)
    start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                  jm_single().config.getint("DAEMON", "daemon_port"),
                  clientfactory,
                  daemon=daemon,
                  p2ep=p2ep)
 def __init__(self):
     self.aborted = False
     self.wallet_service = WalletService(DummyWallet())
     self.offerlist = self.create_my_orders()
示例#23
0
def main():
    parser = OptionParser(
        usage=
        'usage: %prog [options] walletname hex-tx input-index output-index net-transfer',
        description=description)
    add_base_options(parser)
    parser.add_option('-m',
                      '--mixdepth',
                      action='store',
                      type='int',
                      dest='mixdepth',
                      help='mixdepth/account to spend from, default=0',
                      default=0)
    parser.add_option('-g',
                      '--gap-limit',
                      action='store',
                      type='int',
                      dest='gaplimit',
                      default=6,
                      help='gap limit for Joinmarket wallet, default 6.')
    parser.add_option(
        '-n',
        '--no-upload',
        action='store_true',
        dest='no_upload',
        default=False,
        help="if set, we don't upload the new proposal to the servers")
    parser.add_option(
        '-f',
        '--txfee',
        action='store',
        type='int',
        dest='txfee',
        default=-1,
        help='Bitcoin miner tx_fee to use for transaction(s). A number higher '
        'than 1000 is used as "satoshi per KB" tx fee. A number lower than that '
        'uses the dynamic fee estimation of your blockchain provider as '
        'confirmation target. This temporarily overrides the "tx_fees" setting '
        'in your joinmarket.cfg. Works the same way as described in it. Check '
        'it for examples.')
    parser.add_option('-a',
                      '--amtmixdepths',
                      action='store',
                      type='int',
                      dest='amtmixdepths',
                      help='number of mixdepths in wallet, default 5',
                      default=5)
    (options, args) = parser.parse_args()
    snicker_plugin = JMPluginService("SNICKER")
    load_program_config(config_path=options.datadir,
                        plugin_services=[snicker_plugin])
    if len(args) != 5:
        jmprint("Invalid arguments, see --help")
        sys.exit(EXIT_ARGERROR)
    wallet_name, hextx, input_index, output_index, net_transfer = args
    input_index, output_index, net_transfer = [
        int(x) for x in [input_index, output_index, net_transfer]
    ]
    check_regtest()

    # 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))
    max_mix_depth = max([options.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)
    snicker_plugin.start_plugin_logging(wallet_service)
    # 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()

    # now that the wallet is available, we can construct a proposal
    # before encrypting it:
    originating_tx = btc.CMutableTransaction.deserialize(hextobin(hextx))
    txid1 = originating_tx.GetTxid()[::-1]
    # the proposer wallet needs to choose a single utxo, from his selected
    # mixdepth, that is bigger than the output amount of tx1 at the given
    # index.
    fee_est = estimate_tx_fee(2, 3, txtype=wallet_service.get_txtype())
    amt_required = originating_tx.vout[output_index].nValue + fee_est

    prop_utxo_dict = wallet_service.select_utxos(options.mixdepth,
                                                 amt_required)
    prop_utxos = list(prop_utxo_dict)
    prop_utxo_vals = [prop_utxo_dict[x] for x in prop_utxos]
    # get the private key for that utxo
    priv = wallet_service.get_key_from_addr(
        wallet_service.script_to_addr(prop_utxo_vals[0]['script']))
    # construct the arguments for the snicker proposal:
    our_input_utxos = [
        btc.CMutableTxOut(x['value'], x['script']) for x in prop_utxo_vals
    ]

    # destination must be a different mixdepth:
    prop_destn_spk = wallet_service.get_new_script(
        (options.mixdepth + 1) % (wallet_service.mixdepth + 1), 1)
    change_spk = wallet_service.get_new_script(options.mixdepth, 1)
    their_input = (txid1, output_index)
    # we also need to extract the pubkey of the chosen input from
    # the witness; we vary this depending on our wallet type:
    pubkey, msg = btc.extract_pubkey_from_witness(originating_tx, input_index)
    if not pubkey:
        log.error("Failed to extract pubkey from transaction: {}".format(msg))
        sys.exit(EXIT_FAILURE)
    encrypted_proposal = wallet_service.create_snicker_proposal(
        prop_utxos,
        their_input,
        our_input_utxos,
        originating_tx.vout[output_index],
        net_transfer,
        fee_est,
        priv,
        pubkey,
        prop_destn_spk,
        change_spk,
        version_byte=1) + b"," + bintohex(pubkey).encode('utf-8')
    if options.no_upload:
        jmprint(encrypted_proposal.decode("utf-8"))
        sys.exit(EXIT_SUCCESS)

    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 else False
    snicker_client = SNICKERPostingClient([encrypted_proposal])
    servers = jm_single().config.get("SNICKER", "servers").split(",")
    snicker_pf = SNICKERClientProtocolFactory(snicker_client, servers)
    start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                  jm_single().config.getint("DAEMON", "daemon_port"),
                  None,
                  snickerfactory=snicker_pf,
                  daemon=daemon)
示例#24
0
def receive_snicker_main():
    usage = """ Use this script to receive proposals for SNICKER
coinjoins, parse them and then broadcast coinjoins
that fit your criteria. See the SNICKER section of
joinmarket.cfg to set your criteria.
The only argument to this script is the (JM) wallet
file against which to check.
Once all proposals have been parsed, the script will
quit.
Usage: %prog [options] wallet file [proposal]
"""
    parser = OptionParser(usage=usage)
    add_base_options(parser)
    parser.add_option('-g', '--gap-limit', action='store', type="int",
                      dest='gaplimit', default=6,
                      help='gap limit for wallet, default=6')
    parser.add_option('-m', '--mixdepth', action='store', type='int',
                      dest='mixdepth', default=0,
                      help="mixdepth to source coins from")
    parser.add_option('-a',
                      '--amtmixdepths',
                      action='store',
                      type='int',
                      dest='amtmixdepths',
                      help='number of mixdepths in wallet, default 5',
                      default=5)
    parser.add_option(
        '-n',
        '--no-upload',
        action='store_true',
        dest='no_upload',
        default=False,
        help="if set, we read the proposal from the command line"
    )

    (options, args) = parser.parse_args()
    if len(args) < 1:
        parser.error('Needs a wallet file as argument')
        sys.exit(EXIT_ARGERROR)
    wallet_name = args[0]
    snicker_plugin = JMPluginService("SNICKER")
    load_program_config(config_path=options.datadir,
                        plugin_services=[snicker_plugin])

    check_and_start_tor()

    check_regtest()

    wallet_path = get_wallet_path(wallet_name, None)
    max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1])
    wallet = open_test_wallet_maybe(
        wallet_path, wallet_name, max_mix_depth,
        wallet_password_stdin=options.wallet_password_stdin,
        gap_limit=options.gaplimit)
    wallet_service = WalletService(wallet)
    snicker_plugin.start_plugin_logging(wallet_service)
    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=not options.recoversync)
    wallet_service.startService()

    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 else False
    snicker_r = SNICKERReceiver(wallet_service)
    if options.no_upload:
        proposal = args[1]
        snicker_r.process_proposals([proposal])
        return
    servers = jm_single().config.get("SNICKER", "servers").split(",")
    snicker_pf = SNICKERClientProtocolFactory(snicker_r, servers, oneshot=True)
    start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                  jm_single().config.getint("DAEMON", "daemon_port"),
                  None, snickerfactory=snicker_pf,
                  daemon=daemon)
示例#25
0
def main():
    parser = get_sendpayment_parser()
    (options, args) = parser.parse_args()
    load_program_config(config_path=options.datadir)

    if options.schedule == '':
        if ((len(args) < 2) or (btc.is_bip21_uri(args[1]) and len(args) != 2)
                or (not btc.is_bip21_uri(args[1]) and len(args) != 3)):
            parser.error(
                "Joinmarket sendpayment (coinjoin) needs arguments:"
                " wallet, amount, destination address or wallet, bitcoin_uri.")
            sys.exit(EXIT_ARGERROR)

    #without schedule file option, use the arguments to create a schedule
    #of a single transaction
    sweeping = False
    bip78url = None
    if options.schedule == '':
        if btc.is_bip21_uri(args[1]):
            parsed = btc.decode_bip21_uri(args[1])
            try:
                amount = parsed['amount']
            except KeyError:
                parser.error("Given BIP21 URI does not contain amount.")
                sys.exit(EXIT_ARGERROR)
            destaddr = parsed['address']
            if "pj" in parsed:
                # note that this is a URL; its validity
                # checking is deferred to twisted.web.client.Agent
                bip78url = parsed["pj"]
                # setting makercount only for fee sanity check.
                # note we ignore any user setting and enforce N=0,
                # as this is a flag in the code for a non-JM coinjoin;
                # for the fee sanity check, note that BIP78 currently
                # will only allow small fee changes, so N=0 won't
                # be very inaccurate.
                jmprint("Attempting to pay via payjoin.", "info")
                options.makercount = 0
        else:
            amount = btc.amount_to_sat(args[1])
            if amount == 0:
                sweeping = True
            destaddr = args[2]
        mixdepth = options.mixdepth
        addr_valid, errormsg = validate_address(destaddr)
        command_to_burn = (is_burn_destination(destaddr) and sweeping
                           and options.makercount == 0)
        if not addr_valid and not command_to_burn:
            jmprint('ERROR: Address invalid. ' + errormsg, "error")
            if is_burn_destination(destaddr):
                jmprint(
                    "The required options for burning coins are zero makers" +
                    " (-N 0), sweeping (amount = 0) and not using BIP78 Payjoin",
                    "info")
            sys.exit(EXIT_ARGERROR)
        if sweeping == False and amount < DUST_THRESHOLD:
            jmprint(
                'ERROR: Amount ' + btc.amount_to_str(amount) +
                ' is below dust threshold ' +
                btc.amount_to_str(DUST_THRESHOLD) + '.', "error")
            sys.exit(EXIT_ARGERROR)
        if (options.makercount != 0
                and options.makercount < jm_single().config.getint(
                    "POLICY", "minimum_makers")):
            jmprint(
                'ERROR: Maker count ' + str(options.makercount) +
                ' below minimum_makers (' +
                str(jm_single().config.getint("POLICY", "minimum_makers")) +
                ') in joinmarket.cfg.', "error")
            sys.exit(EXIT_ARGERROR)
        schedule = [[
            options.mixdepth, amount, options.makercount, destaddr, 0.0,
            NO_ROUNDING, 0
        ]]
    else:
        if len(args) > 1:
            parser.error("Schedule files are not compatible with "
                         "payment destination/amount arguments.")
            sys.exit(EXIT_ARGERROR)
        result, schedule = get_schedule(options.schedule)
        if not result:
            log.error(
                "Failed to load schedule file, quitting. Check the syntax.")
            log.error("Error was: " + str(schedule))
            sys.exit(EXIT_FAILURE)
        mixdepth = 0
        for s in schedule:
            if s[1] == 0:
                sweeping = True
            #only used for checking the maximum mixdepth required
            mixdepth = max([mixdepth, s[0]])

    wallet_name = args[0]

    check_regtest()

    if options.pickorders:
        chooseOrdersFunc = pick_order
        if sweeping:
            jmprint('WARNING: You may have to pick offers multiple times',
                    "warning")
            jmprint('WARNING: due to manual offer picking while sweeping',
                    "warning")
    else:
        chooseOrdersFunc = options.order_choose_fn

    # If tx_fees are set manually by CLI argument, override joinmarket.cfg:
    if int(options.txfee) > 0:
        jm_single().config.set("POLICY", "tx_fees", str(options.txfee))

    maxcjfee = (1, float('inf'))
    if not options.pickorders and options.makercount != 0:
        maxcjfee = get_max_cj_fee_values(jm_single().config, options)
        log.info("Using maximum coinjoin fee limits per maker of {:.4%}, {} "
                 "".format(maxcjfee[0], btc.amount_to_str(maxcjfee[1])))

    log.info('starting sendpayment')

    max_mix_depth = max([mixdepth, options.amtmixdepths - 1])

    wallet_path = get_wallet_path(wallet_name, None)
    wallet = open_test_wallet_maybe(
        wallet_path,
        wallet_name,
        max_mix_depth,
        wallet_password_stdin=options.wallet_password_stdin,
        gap_limit=options.gaplimit)
    wallet_service = WalletService(wallet)
    if wallet_service.rpc_error:
        sys.exit(EXIT_FAILURE)
    # in this script, we need the wallet synced before
    # logic processing for some paths, so do it now:
    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=not options.recoversync)
    # the sync call here will now be a no-op:
    wallet_service.startService()

    # Dynamically estimate a realistic fee, for coinjoins.
    # At this point we do not know even the number of our own inputs, so
    # we guess conservatively with 2 inputs and 2 outputs each.
    if options.makercount != 0:
        fee_per_cp_guess = estimate_tx_fee(2,
                                           2,
                                           txtype=wallet_service.get_txtype())
        log.debug("Estimated miner/tx fee for each cj participant: " +
                  btc.amount_to_str(fee_per_cp_guess))

    # From the estimated tx fees, check if the expected amount is a
    # significant value compared the the cj amount; currently enabled
    # only for single join (the predominant, non-advanced case)
    if options.schedule == '' and options.makercount != 0:
        total_cj_amount = amount
        if total_cj_amount == 0:
            total_cj_amount = wallet_service.get_balance_by_mixdepth()[
                options.mixdepth]
            if total_cj_amount == 0:
                raise ValueError(
                    "No confirmed coins in the selected mixdepth. Quitting")
        exp_tx_fees_ratio = (
            (1 + options.makercount) * fee_per_cp_guess) / total_cj_amount
        if exp_tx_fees_ratio > 0.05:
            jmprint(
                'WARNING: Expected bitcoin network miner fees for this coinjoin'
                ' amount are roughly {:.1%}'.format(exp_tx_fees_ratio),
                "warning")
            if input('You might want to modify your tx_fee'
                     ' settings in joinmarket.cfg. Still continue? (y/n):'
                     )[0] != 'y':
                sys.exit('Aborted by user.')
        else:
            log.info(
                "Estimated miner/tx fees for this coinjoin amount: {:.1%}".
                format(exp_tx_fees_ratio))

    if options.makercount == 0 and not bip78url:
        tx = direct_send(wallet_service,
                         amount,
                         mixdepth,
                         destaddr,
                         options.answeryes,
                         with_final_psbt=options.with_psbt)
        if options.with_psbt:
            log.info(
                "This PSBT is fully signed and can be sent externally for "
                "broadcasting:")
            log.info(tx.to_base64())
        return

    if wallet.get_txtype() == 'p2pkh':
        jmprint(
            "Only direct sends (use -N 0) are supported for "
            "legacy (non-segwit) wallets.", "error")
        sys.exit(EXIT_ARGERROR)

    def filter_orders_callback(orders_fees, cjamount):
        orders, total_cj_fee = orders_fees
        log.info("Chose these orders: " + pprint.pformat(orders))
        log.info('total cj fee = ' + str(total_cj_fee))
        total_fee_pc = 1.0 * total_cj_fee / cjamount
        log.info('total coinjoin fee = ' +
                 str(float('%.3g' % (100.0 * total_fee_pc))) + '%')
        WARNING_THRESHOLD = 0.02  # 2%
        if total_fee_pc > WARNING_THRESHOLD:
            log.info('\n'.join(['=' * 60] * 3))
            log.info('WARNING   ' * 6)
            log.info('\n'.join(['=' * 60] * 1))
            log.info(
                'OFFERED COINJOIN FEE IS UNUSUALLY HIGH. DOUBLE/TRIPLE CHECK.')
            log.info('\n'.join(['=' * 60] * 1))
            log.info('WARNING   ' * 6)
            log.info('\n'.join(['=' * 60] * 3))
        if not options.answeryes:
            if input('send with these orders? (y/n):')[0] != 'y':
                return False
        return True

    def taker_finished(res, fromtx=False, waittime=0.0, txdetails=None):
        if fromtx == "unconfirmed":
            #If final entry, stop *here*, don't wait for confirmation
            if taker.schedule_index + 1 == len(taker.schedule):
                reactor.stop()
            return
        if fromtx:
            if res:
                txd, txid = txdetails
                reactor.callLater(waittime * 60,
                                  clientfactory.getClient().clientStart)
            else:
                #a transaction failed; we'll try to repeat without the
                #troublemakers.
                #If this error condition is reached from Phase 1 processing,
                #and there are less than minimum_makers honest responses, we
                #just give up (note that in tumbler we tweak and retry, but
                #for sendpayment the user is "online" and so can manually
                #try again).
                #However if the error is in Phase 2 and we have minimum_makers
                #or more responses, we do try to restart with the honest set, here.
                if taker.latest_tx is None:
                    #can only happen with < minimum_makers; see above.
                    log.info("A transaction failed but there are insufficient "
                             "honest respondants to continue; giving up.")
                    reactor.stop()
                    return
                #This is Phase 2; do we have enough to try again?
                taker.add_honest_makers(
                    list(
                        set(taker.maker_utxo_data.keys()).symmetric_difference(
                            set(taker.nonrespondants))))
                if len(taker.honest_makers) < jm_single().config.getint(
                        "POLICY", "minimum_makers"):
                    log.info("Too few makers responded honestly; "
                             "giving up this attempt.")
                    reactor.stop()
                    return
                jmprint("We failed to complete the transaction. The following "
                      "makers responded honestly: " + str(taker.honest_makers) +\
                      ", so we will retry with them.", "warning")
                #Now we have to set the specific group we want to use, and hopefully
                #they will respond again as they showed honesty last time.
                #we must reset the number of counterparties, as well as fix who they
                #are; this is because the number is used to e.g. calculate fees.
                #cleanest way is to reset the number in the schedule before restart.
                taker.schedule[taker.schedule_index][2] = len(
                    taker.honest_makers)
                log.info("Retrying with: " +
                         str(taker.schedule[taker.schedule_index][2]) +
                         " counterparties.")
                #rewind to try again (index is incremented in Taker.initialize())
                taker.schedule_index -= 1
                taker.set_honest_only(True)
                reactor.callLater(5.0, clientfactory.getClient().clientStart)
        else:
            if not res:
                log.info("Did not complete successfully, shutting down")
            #Should usually be unreachable, unless conf received out of order;
            #because we should stop on 'unconfirmed' for last (see above)
            else:
                log.info("All transactions completed correctly")
            reactor.stop()

    if bip78url:
        # TODO sanity check wallet type is segwit
        manager = parse_payjoin_setup(args[1], wallet_service,
                                      options.mixdepth)
        reactor.callWhenRunning(send_payjoin, manager)
        reactor.run()
        return

    else:
        taker = Taker(wallet_service,
                      schedule,
                      order_chooser=chooseOrdersFunc,
                      max_cj_fee=maxcjfee,
                      callbacks=(filter_orders_callback, None, taker_finished))
    clientfactory = JMClientProtocolFactory(taker)
    nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
    daemon = True if nodaemon == 1 else False
    if jm_single().config.get("BLOCKCHAIN", "network") == "regtest":
        startLogging(sys.stdout)
    start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
                  jm_single().config.getint("DAEMON", "daemon_port"),
                  clientfactory,
                  daemon=daemon)
示例#26
0
def make_wallets(n,
                 wallet_structures=None,
                 mean_amt=1,
                 sdev_amt=0,
                 start_index=0,
                 fixed_seeds=None,
                 test_wallet=False,
                 passwords=None,
                 walletclass=SegwitWallet,
                 mixdepths=5,
                 fb_indices=[]):
    '''n: number of wallets to be created
       wallet_structure: array of n arrays , each subarray
       specifying the number of addresses to be populated with coins
       at each depth (for now, this will only populate coins into 'receive' addresses)
       mean_amt: the number of coins (in btc units) in each address as above
       sdev_amt: if randomness in amouts is desired, specify here.
       fb_indices: a list of integers in range(n), and for each of those we will
       use the fidelity bond wallet type (note: to actually *use* the FB feature
       the calling code will have to create the FB utxo, itself). Only supported
       if walletclass=SegwitWallet.
       Returns: a dict of dicts of form {0:{'seed':seed,'wallet':Wallet object},1:..,}
       Default Wallet constructor is joinmarket.Wallet, else use TestWallet,
       which takes a password parameter as in the list passwords.
       '''
    # FIXME: this is basically the same code as jmclient/test/commontest.py
    if len(wallet_structures) != n:
        raise Exception("Number of wallets doesn't match wallet structures")
    if not fixed_seeds:
        seeds = chunks(
            binascii.hexlify(os.urandom(BIP32Wallet.ENTROPY_BYTES * n)),
            BIP32Wallet.ENTROPY_BYTES * 2)
    else:
        seeds = fixed_seeds
    wallets = {}
    for i in range(n):
        assert len(seeds[i]) == BIP32Wallet.ENTROPY_BYTES * 2

        # FIXME: pwd is ignored (but do we really need this anyway?)
        if test_wallet and passwords and i < len(passwords):
            pwd = passwords[i]
        else:
            pwd = None
        if i in fb_indices:
            assert walletclass == SegwitWallet, "Cannot use FB except for native segwit."
            wc = WALLET_IMPLEMENTATIONS[get_configured_wallet_type(True)]
            print("for index: {}, we got wallet type: {}".format(i, wc))
        else:
            wc = walletclass
        w = open_test_wallet_maybe(seeds[i],
                                   seeds[i],
                                   mixdepths - 1,
                                   test_wallet_cls=wc)

        wallet_service = WalletService(w)
        wallets[i + start_index] = {
            'seed': seeds[i].decode('ascii'),
            'wallet': wallet_service
        }
        for j in range(mixdepths):
            for k in range(wallet_structures[i][j]):
                deviation = sdev_amt * random.random()
                amt = mean_amt - sdev_amt / 2.0 + deviation
                if amt < 0: amt = 0.001
                amt = float(Decimal(amt).quantize(Decimal(10)**-8))
                jm_single().bc_interface.grab_coins(
                    wallet_service.get_new_addr(
                        j, BaseWallet.ADDRESS_TYPE_INTERNAL), amt)
    return wallets