diff --git a/jmclient/__init__.py b/jmclient/__init__.py index d6e771a..1ec2496 100644 --- a/jmclient/__init__.py +++ b/jmclient/__init__.py @@ -25,6 +25,7 @@ from .blockchaininterface import (BlockrInterface, BlockchainInterface, sync_wal from .client_protocol import JMTakerClientProtocolFactory, start_reactor from .podle import set_commitment_file, get_commitment_file from .commands import * +from .schedule import get_schedule # Set default logging handler to avoid "No handler found" warnings. try: diff --git a/jmclient/schedule.py b/jmclient/schedule.py new file mode 100644 index 0000000..00bef87 --- /dev/null +++ b/jmclient/schedule.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +from __future__ import print_function +from jmclient import validate_address +"""Utility functions for dealing with Taker schedules. + +- attempt to read the schedule from the provided file +- (TODO) generate a schedule for e.g. tumbling from a given wallet, with parameters +""" + +def get_schedule(filename): + with open(filename, "rb") as f: + schedule = [] + schedule_lines = f.readlines() + for sl in schedule_lines: + if sl.startswith("#"): + continue + try: + mixdepth, amount, makercount, destaddr = sl.split(',') + except ValueError as e: + return (False, "Failed to parse schedule line: " + sl) + try: + mixdepth = int(mixdepth) + amount = int(amount) + makercount = int(makercount) + destaddr = destaddr.strip() + except ValueError as e: + return (False, "Failed to parse schedule line: " + sl) + success, errmsg = validate_address(destaddr) + if not success: + return (False, "Invalid address: " + destaddr + "," + errmsg) + schedule.append((mixdepth, amount, makercount, destaddr)) + return (True, schedule) diff --git a/scripts/joinmarketd.py b/scripts/joinmarketd.py index 4e463c5..18448c6 100644 --- a/scripts/joinmarketd.py +++ b/scripts/joinmarketd.py @@ -123,6 +123,11 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch): return {'accepted': True} def init_connections(self, nick): + """Sets up message channel connections + if they are not already up; re-sets joinmarket state to 0 + for a new transaction; effectively means any previous + incomplete transaction is wiped. + """ self.jm_state = 0 #uninited if self.restart_mc_required: MCThread(self.mcc).start() @@ -155,6 +160,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch): #Request orderbook here, on explicit setup request from client, #assumes messagechannels are in "up" state. Orders are read #in the callback on_order_seen in OrderbookWatch. + #TODO: pubmsg should not (usually?) fire if already up from previous run. self.mcc.pubmsg(COMMAND_PREFIX + "orderbook") self.jm_state = 1 return {'accepted': True} @@ -222,6 +228,10 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch): self.respondToIoauths(True) def respondToIoauths(self, accepted): + if self.jm_state != 2: + #this can be called a second time on timeout, in which case we + #do nothing + return d = self.callRemote(JMFillResponse, success=accepted, ioauth_data = json.dumps(self.ioauth_data)) @@ -246,10 +256,14 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch): if not accepted: log.msg("Taker rejected utxos provided; resetting.") #TODO create re-set function to start again + else: + #only update state if client accepted + self.jm_state = 3 @JMMakeTx.responder def on_JM_MAKE_TX(self, nick_list, txhex): - if not self.jm_state == 2: + if not self.jm_state == 3: + log.msg("Make tx was called in wrong state, rejecting") return {'accepted': False} nick_list = json.loads(nick_list) self.mcc.send_tx(nick_list, txhex) diff --git a/scripts/sample-schedule-for-testnet b/scripts/sample-schedule-for-testnet new file mode 100644 index 0000000..a0f6fdf --- /dev/null +++ b/scripts/sample-schedule-for-testnet @@ -0,0 +1,2 @@ +#sample for testing +1, 50000000, 3, n18jvNgdCWkb5YWEMVARjBfcizg4kHcYRZ diff --git a/scripts/sendpayment.py b/scripts/sendpayment.py index e96999d..e2969bc 100644 --- a/scripts/sendpayment.py +++ b/scripts/sendpayment.py @@ -47,7 +47,7 @@ from optparse import OptionParser from twisted.internet import reactor import time -from jmclient import (Taker, load_program_config, +from jmclient import (Taker, load_program_config, get_schedule, JMTakerClientProtocolFactory, start_reactor, validate_address, jm_single, choose_orders, choose_sweep_orders, pick_order, @@ -103,6 +103,12 @@ def main(): dest='daemonport', help='port on which joinmarketd is running', default='12345') + parser.add_option('-S', + '--schedule-file', + type='str', + dest='schedule', + help='schedule file name', + default='') parser.add_option( '-C', '--choose-cheapest', @@ -153,26 +159,50 @@ def main(): help=('Use the Bitcoin Core wallet through json rpc, instead ' 'of the internal joinmarket wallet. Requires ' 'blockchain_source=json-rpc')) + (options, args) = parser.parse_args() + load_program_config() - if len(args) < 3: + if options.schedule == '' and len(args) < 3: parser.error('Needs a wallet, amount and destination address') sys.exit(0) + + #without schedule file option, use the arguments to create a schedule + #of a single transaction + sweeping = False + if options.schedule == '': + amount = int(args[1]) + if amount == 0: + sweeping = True + destaddr = args[2] + mixdepth = options.mixdepth + addr_valid, errormsg = validate_address(destaddr) + if not addr_valid: + print('ERROR: Address invalid. ' + errormsg) + return + schedule = [(options.mixdepth, amount, options.makercount, destaddr)] + else: + result, schedule = get_schedule(options.schedule) + if not result: + log.info("Failed to load schedule file, quitting. Check the syntax.") + log.info("Error was: " + str(schedule)) + sys.exit(0) + 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] - amount = int(args[1]) - destaddr = args[2] - load_program_config() + #for testing, TODO remove jm_single().maker_timeout_sec = 5 - addr_valid, errormsg = validate_address(destaddr) - if not addr_valid: - print('ERROR: Address invalid. ' + errormsg) - return chooseOrdersFunc = None if options.pickorders: chooseOrdersFunc = pick_order - if amount == 0: + if sweeping: print('WARNING: You may have to pick offers multiple times') print('WARNING: due to manual offer picking while sweeping') elif options.choosecheapest: @@ -190,9 +220,10 @@ def main(): assert (options.txfee >= 0) log.debug('starting sendpayment') - global wallet + if not options.userpcwallet: - wallet = Wallet(wallet_name, options.amtmixdepths, options.gaplimit) + max_mix_depth = max([mixdepth, options.amtmixdepths]) + wallet = Wallet(wallet_name, max_mix_depth, options.gaplimit) else: wallet = BitcoinCoreWallet(fromaccount=wallet_name) jm_single().bc_interface.sync_wallet(wallet) @@ -212,9 +243,6 @@ def main(): log.info("All transactions completed correctly") reactor.stop() - #just a sample schedule; twice from same mixdepth - schedule = [(options.mixdepth, amount, options.makercount, destaddr), - (options.mixdepth, amount, options.makercount, destaddr)] if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface): #to allow testing of confirm/unconfirm callback for multiple txs jm_single().bc_interface.tick_forward_chain_interval = 10 diff --git a/scripts/wallets/.gitignore b/scripts/wallets/.gitignore new file mode 100644 index 0000000..86d0cb2 --- /dev/null +++ b/scripts/wallets/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file