Browse Source
Refactor parsers into separate module (more code reuse to do). Add a tumbler schedule generator in jmclient/schedule.py. Alter syntax of schedules; taker interprets fractional amounts as percentage of mixdepth (for tumbler), and integers as satoshi, zero still sweep. TODO tumbler is only a minor delta from sendpayment, fold them together.master
7 changed files with 481 additions and 110 deletions
@ -0,0 +1,276 @@ |
|||||||
|
#! /usr/bin/env python |
||||||
|
from __future__ import absolute_import, print_function |
||||||
|
|
||||||
|
"""This exists as a separate module for two reasons: |
||||||
|
to reduce clutter in main scripts, and (TODO) refactor out |
||||||
|
options which are common to more than one script in a base class. |
||||||
|
""" |
||||||
|
|
||||||
|
from optparse import OptionParser |
||||||
|
|
||||||
|
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 spend from. Useful if a previous tumbler run prematurely ended with ' |
||||||
|
+ |
||||||
|
'coins being left in higher mixing levels, this option can be used to resume without needing' |
||||||
|
+ ' to send to another address. 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( |
||||||
|
'-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( |
||||||
|
'-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', |
||||||
|
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') |
||||||
|
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')) |
||||||
|
return parser |
||||||
|
|
||||||
|
def get_sendpayment_parser(): |
||||||
|
parser = OptionParser( |
||||||
|
usage= |
||||||
|
'usage: %prog [options] [wallet file / fromaccount] [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( |
||||||
|
'-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', |
||||||
|
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', |
||||||
|
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', |
||||||
|
action='store_true', |
||||||
|
dest='pickorders', |
||||||
|
default=False, |
||||||
|
help= |
||||||
|
'manually 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( |
||||||
|
'--rpcwallet', |
||||||
|
action='store_true', |
||||||
|
dest='userpcwallet', |
||||||
|
default=False, |
||||||
|
help=('Use the Bitcoin Core wallet through json rpc, instead ' |
||||||
|
'of the internal joinmarket wallet. Requires ' |
||||||
|
'blockchain_source=json-rpc')) |
||||||
|
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')) |
||||||
|
return parser |
||||||
|
|
||||||
|
|
||||||
@ -1,3 +1,5 @@ |
|||||||
#sample for testing |
#sample for testing |
||||||
1, 110000000, 3, INTERNAL |
#fields: source mixdepth, amount (satoshis or fraction if non-int), |
||||||
0, 20000000, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw |
# makercount, destination, waittime (ignored if 0) |
||||||
|
1, 110000000, 3, INTERNAL, 0 |
||||||
|
0, 20000000, 2, mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw, 0 |
||||||
@ -0,0 +1,103 @@ |
|||||||
|
from __future__ import absolute_import, print_function |
||||||
|
|
||||||
|
import random |
||||||
|
import sys |
||||||
|
import threading |
||||||
|
from optparse import OptionParser |
||||||
|
from twisted.internet import reactor |
||||||
|
import time |
||||||
|
import os |
||||||
|
import pprint |
||||||
|
import copy |
||||||
|
|
||||||
|
from jmclient import (Taker, load_program_config, get_schedule, weighted_order_choose, |
||||||
|
JMTakerClientProtocolFactory, start_reactor, |
||||||
|
validate_address, jm_single, WalletError, |
||||||
|
Wallet, sync_wallet, get_tumble_schedule, |
||||||
|
RegtestBitcoinCoreInterface, estimate_tx_fee) |
||||||
|
|
||||||
|
from jmbase.support import get_log, debug_dump_object, get_password |
||||||
|
from cli_options import get_tumbler_parser |
||||||
|
log = get_log() |
||||||
|
|
||||||
|
def main(): |
||||||
|
(options, args) = get_tumbler_parser().parse_args() |
||||||
|
options = vars(options) |
||||||
|
|
||||||
|
if len(args) < 1: |
||||||
|
parser.error('Needs a wallet file') |
||||||
|
sys.exit(0) |
||||||
|
|
||||||
|
load_program_config() |
||||||
|
|
||||||
|
#Load the wallet |
||||||
|
wallet_name = args[0] |
||||||
|
max_mix_depth = options['mixdepthsrc'] + options['mixdepthcount'] |
||||||
|
if not os.path.exists(os.path.join('wallets', wallet_name)): |
||||||
|
wallet = Wallet(wallet_name, None, max_mix_depth) |
||||||
|
else: |
||||||
|
while True: |
||||||
|
try: |
||||||
|
pwd = get_password("Enter wallet decryption passphrase: ") |
||||||
|
wallet = Wallet(wallet_name, pwd, max_mix_depth) |
||||||
|
except WalletError: |
||||||
|
print("Wrong password, try again.") |
||||||
|
continue |
||||||
|
except Exception as e: |
||||||
|
print("Failed to load wallet, error message: " + repr(e)) |
||||||
|
sys.exit(0) |
||||||
|
break |
||||||
|
sync_wallet(wallet, fast=options['fastsync']) |
||||||
|
|
||||||
|
#Parse options and generate schedule |
||||||
|
|
||||||
|
#for testing, TODO remove |
||||||
|
jm_single().maker_timeout_sec = 5 |
||||||
|
|
||||||
|
jm_single().mincjamount = options['mincjamount'] |
||||||
|
destaddrs = args[1:] |
||||||
|
print(destaddrs) |
||||||
|
schedule = get_tumble_schedule(options, destaddrs) |
||||||
|
print("got schedule:") |
||||||
|
print(pprint.pformat(schedule)) |
||||||
|
|
||||||
|
#callback for order checking; dummy/passthrough |
||||||
|
def filter_orders_callback(orders_fees, cjamount): |
||||||
|
return True |
||||||
|
#callback between transactions |
||||||
|
def taker_finished(res, fromtx=False, waittime=0.0): |
||||||
|
if fromtx: |
||||||
|
if res: |
||||||
|
sync_wallet(wallet, fast=options['fastsync']) |
||||||
|
log.info("Waiting for: " + str(waittime) + " seconds.") |
||||||
|
reactor.callLater(waittime, clientfactory.getClient().clientStart) |
||||||
|
else: |
||||||
|
#a transaction failed; just stop |
||||||
|
reactor.stop() |
||||||
|
else: |
||||||
|
if not res: |
||||||
|
log.info("Did not complete successfully, shutting down") |
||||||
|
else: |
||||||
|
log.info("All transactions completed correctly") |
||||||
|
reactor.stop() |
||||||
|
|
||||||
|
#to allow testing of confirm/unconfirm callback for multiple txs |
||||||
|
if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface): |
||||||
|
jm_single().bc_interface.tick_forward_chain_interval = 10 |
||||||
|
|
||||||
|
#instantiate Taker with given schedule and run |
||||||
|
taker = Taker(wallet, |
||||||
|
schedule, |
||||||
|
order_chooser=weighted_order_choose, |
||||||
|
callbacks=(filter_orders_callback, None, taker_finished)) |
||||||
|
clientfactory = JMTakerClientProtocolFactory(taker) |
||||||
|
nodaemon = jm_single().config.getint("DAEMON", "no_daemon") |
||||||
|
daemon = True if nodaemon == 1 else False |
||||||
|
start_reactor(jm_single().config.get("DAEMON", "daemon_host"), |
||||||
|
jm_single().config.getint("DAEMON", "daemon_port"), |
||||||
|
clientfactory, daemon=daemon) |
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
main() |
||||||
|
print('done') |
||||||
Loading…
Reference in new issue