|
|
|
|
@ -1,13 +1,201 @@
|
|
|
|
|
#! /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 (TODO) refactor out |
|
|
|
|
to reduce clutter in main scripts, and refactor out |
|
|
|
|
options which are common to more than one script in a base class. |
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
from optparse import OptionParser |
|
|
|
|
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( |
|
|
|
|
@ -27,17 +215,6 @@ def get_tumbler_parser():
|
|
|
|
|
help= |
|
|
|
|
'Mixing depth to start tumble process from. default=0.', |
|
|
|
|
default=0) |
|
|
|
|
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('--restart', |
|
|
|
|
action='store_true', |
|
|
|
|
dest='restart', |
|
|
|
|
@ -61,16 +238,6 @@ def get_tumbler_parser():
|
|
|
|
|
'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( |
|
|
|
|
'-x', |
|
|
|
|
'--maxcjfee', |
|
|
|
|
type='float', |
|
|
|
|
dest='maxcjfee', |
|
|
|
|
nargs=2, |
|
|
|
|
default=(0.01, 10000), |
|
|
|
|
help='maximum coinjoin fee and bitcoin value the tumbler is ' |
|
|
|
|
'willing to pay to a single market maker. Both values need to be exceeded, so if ' |
|
|
|
|
'the fee is 30% but only 500satoshi is paid the tx will go ahead. default=0.01, 10000 (1%, 10000satoshi)') |
|
|
|
|
parser.add_option( |
|
|
|
|
'-N', |
|
|
|
|
'--makercountrange', |
|
|
|
|
@ -174,14 +341,10 @@ def get_tumbler_parser():
|
|
|
|
|
default=9, |
|
|
|
|
help= |
|
|
|
|
'maximum amount of times to re-create a transaction before giving up, default 9') |
|
|
|
|
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')) |
|
|
|
|
add_common_options(parser) |
|
|
|
|
return parser |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_sendpayment_parser(): |
|
|
|
|
parser = OptionParser( |
|
|
|
|
usage= |
|
|
|
|
@ -191,19 +354,6 @@ def get_sendpayment_parser():
|
|
|
|
|
'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( |
|
|
|
|
'-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( |
|
|
|
|
'-w', |
|
|
|
|
'--wait-time', |
|
|
|
|
@ -226,14 +376,6 @@ def get_sendpayment_parser():
|
|
|
|
|
dest='schedule', |
|
|
|
|
help='schedule file name; see file "sample-schedule-for-testnet" for explanation and example', |
|
|
|
|
default='') |
|
|
|
|
parser.add_option( |
|
|
|
|
'-C', |
|
|
|
|
'--choose-cheapest', |
|
|
|
|
action='store_true', |
|
|
|
|
dest='choosecheapest', |
|
|
|
|
default=False, |
|
|
|
|
help= |
|
|
|
|
'override weightened offers picking and choose cheapest. this might reduce anonymity.') |
|
|
|
|
parser.add_option( |
|
|
|
|
'-P', |
|
|
|
|
'--pick-orders', |
|
|
|
|
@ -268,12 +410,5 @@ def get_sendpayment_parser():
|
|
|
|
|
dest='answeryes', |
|
|
|
|
default=False, |
|
|
|
|
help='answer yes to everything') |
|
|
|
|
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')) |
|
|
|
|
add_common_options(parser) |
|
|
|
|
return parser |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|