You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
414 lines
15 KiB
414 lines
15 KiB
#! /usr/bin/env python |
|
from __future__ import absolute_import, print_function |
|
import random |
|
from optparse import OptionParser, OptionValueError |
|
from ConfigParser import NoOptionError |
|
|
|
import jmclient.support |
|
|
|
"""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=str, |
|
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 len(filter(lambda x: x is None, 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 = raw_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 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='str', |
|
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='str', |
|
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') |
|
add_common_options(parser) |
|
return parser
|
|
|