mirror of
https://github.com/JoinMarket-Org/joinmarket-clientserver.git
synced 2026-08-14 12:43:38 +02:00
- update mktx() to allow optional locktime setting (and sequence) - add a mk_shuffled_tx method to the wallet module - add a P2EPTaker and P2EPMaker class (inherit from Taker, Maker) - add a -T option to sendpayment script for doing payjoins - add a receive_payjoin script for receivers. - add payjoin tests in jmclient/test/test_payjoin.py - add a custom utxo selection method select_one_utxo to support.py - support bech32 wallets (SegwitWallet, p2wpkh) with native=true in config POLICY for PayJoin and direct send (not Joinmarket CJ) - add a PayJoin.md usage guide in docs/ - include version bytes in pubkey message for forward compat - taker pays fees but controls size (utxo number and fee/kB) - add P2WPKH fee estimator - Enforce INFO level logging in payjoin - refactors regtest config settings into one place - bugfix: script_to_address vbyte argument is bytes not integer
441 lines
16 KiB
Python
441 lines
16 KiB
Python
#! /usr/bin/env python
|
|
from __future__ import (absolute_import, division,
|
|
print_function, unicode_literals)
|
|
from builtins import * # noqa: F401
|
|
import random
|
|
from optparse import OptionParser, OptionValueError
|
|
from configparser import NoOptionError
|
|
|
|
import jmclient.support
|
|
from jmclient import jm_single, RegtestBitcoinCoreInterface, cryptoengine
|
|
|
|
"""This exists as a separate module for two reasons:
|
|
to reduce clutter in main scripts, and refactor out
|
|
options which are common to more than one script in a base class.
|
|
"""
|
|
|
|
order_choose_algorithms = {
|
|
'random_under_max_order_choose': '-R',
|
|
'cheapest_order_choose': '-C',
|
|
'weighted_order_choose': '-W'
|
|
}
|
|
|
|
|
|
def add_common_options(parser):
|
|
parser.add_option(
|
|
'-f',
|
|
'--txfee',
|
|
action='store',
|
|
type='int',
|
|
dest='txfee',
|
|
default=-1,
|
|
help='number of satoshis per participant to use as the initial estimate '
|
|
'for the total transaction fee, default=dynamically estimated, note that this is adjusted '
|
|
'based on the estimated fee calculated after tx construction, based on '
|
|
'policy set in joinmarket.cfg.')
|
|
parser.add_option('--fast',
|
|
action='store_true',
|
|
dest='fastsync',
|
|
default=False,
|
|
help=('choose to do fast wallet sync, only for Core and '
|
|
'only for previously synced wallet'))
|
|
parser.add_option(
|
|
'-x',
|
|
'--max-cj-fee-abs',
|
|
type='int',
|
|
dest='max_cj_fee_abs',
|
|
help="Maximum absolute coinjoin fee in satoshi to pay to a single "
|
|
"market maker for a transaction. Both the limits given in "
|
|
"--max-cj-fee-abs and --max-cj-fee-rel must be exceeded in order "
|
|
"to not consider a certain offer.")
|
|
parser.add_option(
|
|
'-r',
|
|
'--max-cj-fee-rel',
|
|
type='float',
|
|
dest='max_cj_fee_rel',
|
|
help="Maximum relative coinjoin fee, in fractions of the coinjoin "
|
|
"value, to pay to a single market maker for a transaction. Both "
|
|
"the limits given in --max-cj-fee-abs and --max-cj-fee-rel must "
|
|
"be exceeded in order to not consider a certain offer.\n"
|
|
"Example: 0.001 for a maximum fee of 0.1% of the cj amount")
|
|
parser.add_option(
|
|
'--order-choose-algorithm',
|
|
action='callback',
|
|
type='string',
|
|
default=jmclient.support.random_under_max_order_choose,
|
|
callback=get_order_choose_algorithm,
|
|
help="Set the algorithm to use for selecting orders from the order book.\n"
|
|
"Default: {}\n"
|
|
"Available options: {}"
|
|
.format('random_under_max_order_choose',
|
|
', '.join(order_choose_algorithms.keys())),
|
|
dest='order_choose_fn')
|
|
add_order_choose_short_options(parser)
|
|
|
|
|
|
def add_order_choose_short_options(parser):
|
|
for name in sorted(order_choose_algorithms.keys()):
|
|
option = order_choose_algorithms[name]
|
|
parser.add_option(
|
|
option,
|
|
help="alias for --order-choose-algorithm={}".format(name),
|
|
nargs=0,
|
|
action='callback',
|
|
callback=get_order_choose_algorithm,
|
|
callback_kwargs={'value_kw': name},
|
|
dest='order_choose_fn')
|
|
|
|
|
|
def get_order_choose_algorithm(option, opt_str, value, parser, value_kw=None):
|
|
value = value_kw or value
|
|
if value not in order_choose_algorithms:
|
|
raise OptionValueError("{} must be one of {}".format(
|
|
opt_str, list(order_choose_algorithms.keys())))
|
|
fn = getattr(jmclient.support, value, None)
|
|
if not fn:
|
|
raise OptionValueError("internal error: '{}' order choose algorithm not"
|
|
" found".format(value))
|
|
setattr(parser.values, option.dest, fn)
|
|
|
|
|
|
def get_max_cj_fee_values(config, parser_options):
|
|
CONFIG_SECTION = 'POLICY'
|
|
CONFIG_OPTION = 'max_cj_fee_'
|
|
# rel, abs
|
|
fee_values = [None, None]
|
|
fee_types = [float, int]
|
|
|
|
for i, option in enumerate(('rel', 'abs')):
|
|
value = getattr(parser_options, CONFIG_OPTION + option, None)
|
|
if value is not None:
|
|
fee_values[i] = fee_types[i](value)
|
|
continue
|
|
|
|
try:
|
|
fee_values[i] = config.get(CONFIG_SECTION, CONFIG_OPTION + option)
|
|
except NoOptionError:
|
|
pass
|
|
|
|
if any(x is None for x in fee_values):
|
|
fee_values = prompt_user_for_cj_fee(*fee_values)
|
|
|
|
return tuple(map(lambda j: fee_types[j](fee_values[j]),
|
|
range(len(fee_values))))
|
|
|
|
|
|
def prompt_user_for_cj_fee(rel_val, abs_val):
|
|
msg = """Joinmarket will choose market makers randomly as long as their
|
|
fees are below a certain maximum value, or fraction. The suggested maximums are:
|
|
|
|
X = {rel_val}
|
|
Y = {abs_val} satoshis
|
|
|
|
Those values were chosen randomly for you (unless you already set one of them
|
|
in your joinmarket.cfg or via a CLI option).
|
|
|
|
Since you are using N counterparties, if you agree to these values, your
|
|
**maximum** coinjoin fee will be either N*Y satoshis or N*X percent of your
|
|
coinjoin amount, depending on which is larger. The actual fee is likely to be
|
|
significantly less; perhaps half that amount, depending on which
|
|
counterparties are selected."""
|
|
|
|
def prompt_user_value(m, val, check):
|
|
while True:
|
|
data = input(m)
|
|
if data == 'y':
|
|
return val
|
|
try:
|
|
val_user = float(data)
|
|
except ValueError:
|
|
print("Bad answer, try again.")
|
|
continue
|
|
if not check(val_user):
|
|
continue
|
|
return val_user
|
|
|
|
rel_prompt = False
|
|
if rel_val is None:
|
|
rel_prompt = True
|
|
rel_val = 0.001
|
|
|
|
abs_prompt = False
|
|
if abs_val is None:
|
|
abs_prompt = True
|
|
abs_val = random.randint(1000, 10000)
|
|
|
|
print(msg.format(rel_val=rel_val, abs_val=abs_val))
|
|
if rel_prompt:
|
|
msg = ("\nIf you want to keep this relative limit, enter 'y';"
|
|
"\notherwise choose your own fraction (between 1 and 0): ")
|
|
|
|
def rel_check(val):
|
|
if val >= 1:
|
|
print("Choose a number below 1! Else you will spend all your "
|
|
"bitcoins for fees!")
|
|
return False
|
|
return True
|
|
|
|
rel_val = prompt_user_value(msg, rel_val, rel_check)
|
|
print("Success! Using relative fee limit of {:%}".format(rel_val))
|
|
|
|
if abs_prompt:
|
|
msg = ("\nIf you want to keep this absolute limit, enter 'y';"
|
|
"\notherwise choose your own limit in satoshi: ")
|
|
|
|
def abs_check(val):
|
|
if val % 1 != 0:
|
|
print("You must choose a full number!")
|
|
return False
|
|
return True
|
|
|
|
abs_val = int(prompt_user_value(msg, abs_val, abs_check))
|
|
print("Success! Using absolute fee limit of {}".format(abs_val))
|
|
|
|
print("""\nIf you don't want to see this message again, make an entry like
|
|
this in the POLICY section of joinmarket.cfg:
|
|
|
|
max_cj_fee_abs = {abs_val}
|
|
max_cj_fee_rel = {rel_val}\n""".format(rel_val=rel_val, abs_val=abs_val))
|
|
|
|
return rel_val, abs_val
|
|
|
|
|
|
def check_regtest(blockchain_start=True):
|
|
""" Applies any regtest-specific configuration
|
|
"""
|
|
if not isinstance(jm_single().bc_interface,
|
|
RegtestBitcoinCoreInterface):
|
|
return
|
|
if blockchain_start:
|
|
#to allow testing of confirm/unconfirm callback for multiple txs
|
|
jm_single().bc_interface.tick_forward_chain_interval = 10
|
|
jm_single().bc_interface.simulating = True
|
|
jm_single().maker_timeout_sec = 5
|
|
# handles the custom regtest hrp for bech32
|
|
cryptoengine.BTC_P2WPKH.VBYTE = 100
|
|
|
|
def get_tumbler_parser():
|
|
parser = OptionParser(
|
|
usage='usage: %prog [options] [wallet file] [destaddr(s)...]',
|
|
description=
|
|
'Sends bitcoins to many different addresses using coinjoin in'
|
|
' an attempt to break the link between them. Sending to multiple '
|
|
' addresses is highly recommended for privacy. This tumbler can'
|
|
' be configured to ask for more address mid-run, giving the user'
|
|
' a chance to click `Generate New Deposit Address` on whatever service'
|
|
' they are using.')
|
|
parser.add_option(
|
|
'-m',
|
|
'--mixdepthsource',
|
|
type='int',
|
|
dest='mixdepthsrc',
|
|
help=
|
|
'Mixing depth to start tumble process from. default=0.',
|
|
default=0)
|
|
parser.add_option('--restart',
|
|
action='store_true',
|
|
dest='restart',
|
|
default=False,
|
|
help=('Restarts the schedule currently found in the schedule file in the '
|
|
'logs directory, with name TUMBLE.schedule or what is set in the '
|
|
'schedulefile option.'))
|
|
parser.add_option('--schedulefile',
|
|
type='string',
|
|
dest='schedulefile',
|
|
default='TUMBLE.schedule',
|
|
help=('Name of schedule file for tumbler, useful for restart, default '
|
|
'TUMBLE.schedule'))
|
|
parser.add_option(
|
|
'-a',
|
|
'--addrcount',
|
|
type='int',
|
|
dest='addrcount',
|
|
default=3,
|
|
help=
|
|
'How many destination addresses in total should be used. If not enough are given'
|
|
' as command line arguments, the script will ask for more. This parameter is required'
|
|
' to stop amount correlation. default=3')
|
|
parser.add_option(
|
|
'-N',
|
|
'--makercountrange',
|
|
type='float',
|
|
nargs=2,
|
|
action='store',
|
|
dest='makercountrange',
|
|
help=
|
|
'Input the mean and spread of number of makers to use. e.g. 6 1 will be a normal distribution '
|
|
'with mean 6 and standard deviation 1 inclusive, default=6 1 (floats are also OK)',
|
|
default=(6, 1))
|
|
parser.add_option(
|
|
'--minmakercount',
|
|
type='int',
|
|
dest='minmakercount',
|
|
default=4,
|
|
help=
|
|
'The minimum maker count in a transaction, random values below this are clamped at this number. default=4')
|
|
parser.add_option(
|
|
'-M',
|
|
'--mixdepthcount',
|
|
type='int',
|
|
dest='mixdepthcount',
|
|
help='How many mixing depths to mix through',
|
|
default=4)
|
|
parser.add_option(
|
|
'-c',
|
|
'--txcountparams',
|
|
type='float',
|
|
nargs=2,
|
|
dest='txcountparams',
|
|
default=(4, 1),
|
|
help=
|
|
'The number of transactions to take coins from one mixing depth to the next, it is'
|
|
' randomly chosen following a normal distribution. Should be similar to --addrask. '
|
|
'This option controls the parameters of the normal distribution curve. (mean, standard deviation). default=4 1')
|
|
parser.add_option(
|
|
'--mintxcount',
|
|
type='int',
|
|
dest='mintxcount',
|
|
default=1,
|
|
help='The minimum transaction count per mixing level, default=1')
|
|
parser.add_option(
|
|
'--donateamount',
|
|
type='float',
|
|
dest='donateamount',
|
|
default=0,
|
|
help=
|
|
'percent of funds to donate to joinmarket development, or zero to opt out (default=0%)')
|
|
parser.add_option(
|
|
'--amountpower',
|
|
type='float',
|
|
dest='amountpower',
|
|
default=100.0,
|
|
help=
|
|
'The output amounts follow a power law distribution, this is the power, default=100.0')
|
|
parser.add_option(
|
|
'-l',
|
|
'--timelambda',
|
|
type='float',
|
|
dest='timelambda',
|
|
default=30,
|
|
help=
|
|
'Average the number of minutes to wait between transactions. Randomly chosen '
|
|
' following an exponential distribution, which describes the time between uncorrelated'
|
|
' events. default=30')
|
|
parser.add_option(
|
|
'-w',
|
|
'--wait-time',
|
|
action='store',
|
|
type='float',
|
|
dest='waittime',
|
|
help='wait time in seconds to allow orders to arrive, default=20',
|
|
default=20)
|
|
parser.add_option(
|
|
'-s',
|
|
'--mincjamount',
|
|
type='int',
|
|
dest='mincjamount',
|
|
default=100000,
|
|
help='minimum coinjoin amount in transaction in satoshi, default 100k')
|
|
parser.add_option(
|
|
'-q',
|
|
'--liquiditywait',
|
|
type='int',
|
|
dest='liquiditywait',
|
|
default=60,
|
|
help=
|
|
'amount of seconds to wait after failing to choose suitable orders before trying again, default 60')
|
|
parser.add_option(
|
|
'--maxbroadcasts',
|
|
type='int',
|
|
dest='maxbroadcasts',
|
|
default=4,
|
|
help=
|
|
'maximum amount of times to broadcast a transaction before giving up and re-creating it, default 4')
|
|
parser.add_option(
|
|
'--maxcreatetx',
|
|
type='int',
|
|
dest='maxcreatetx',
|
|
default=9,
|
|
help=
|
|
'maximum amount of times to re-create a transaction before giving up, default 9')
|
|
add_common_options(parser)
|
|
return parser
|
|
|
|
|
|
def get_sendpayment_parser():
|
|
parser = OptionParser(
|
|
usage=
|
|
'usage: %prog [options] [wallet file] [amount] [destaddr]',
|
|
description='Sends a single payment from a given mixing depth of your '
|
|
+
|
|
'wallet to an given address using coinjoin and then switches off. Also sends from bitcoinqt. '
|
|
+
|
|
'Setting amount to zero will do a sweep, where the entire mix depth is emptied')
|
|
parser.add_option(
|
|
'-w',
|
|
'--wait-time',
|
|
action='store',
|
|
type='float',
|
|
dest='waittime',
|
|
help='wait time in seconds to allow orders to arrive, default=15',
|
|
default=15)
|
|
parser.add_option(
|
|
'-N',
|
|
'--makercount',
|
|
action='store',
|
|
type='int',
|
|
dest='makercount',
|
|
help='how many makers to coinjoin with, default random from 4 to 6',
|
|
default=random.randint(4, 6))
|
|
parser.add_option('-S',
|
|
'--schedule-file',
|
|
type='string',
|
|
dest='schedule',
|
|
help='schedule file name; see file "sample-schedule-for-testnet" for explanation and example',
|
|
default='')
|
|
parser.add_option(
|
|
'-P',
|
|
'--pick-orders',
|
|
action='store_true',
|
|
dest='pickorders',
|
|
default=False,
|
|
help=
|
|
'interactively pick which orders to take. doesn\'t work while sweeping.')
|
|
parser.add_option('-m',
|
|
'--mixdepth',
|
|
action='store',
|
|
type='int',
|
|
dest='mixdepth',
|
|
help='mixing depth to spend from, default=0',
|
|
default=0)
|
|
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)
|
|
parser.add_option('--yes',
|
|
action='store_true',
|
|
dest='answeryes',
|
|
default=False,
|
|
help='answer yes to everything')
|
|
parser.add_option('--payjoin',
|
|
'-T',
|
|
type='str',
|
|
action='store',
|
|
dest='p2ep',
|
|
default='',
|
|
help='specify recipient IRC nick for a '
|
|
'p2ep style payment, for example:\n'
|
|
'J5Ehn3EieVZFtm4q ')
|
|
|
|
add_common_options(parser)
|
|
return parser
|