From 027a7643fde2ab3f46787648d833fa9acd07a04a Mon Sep 17 00:00:00 2001 From: Adam Gibson Date: Fri, 25 Nov 2016 15:49:18 +0200 Subject: [PATCH] schedules read from files; joinmarketd state bugfix on ioauth receipt New module jmclient.schedule currently only parses schedule files, and returns an array of tuples as a schedule. Option added to sendpayment (-S) to pass in a schedule file, instead of using command line arguments (still valid for single joins). Added intermediate state to daemon to track whether ioauths have been already sent, to prevent duplicate sending on timeout. --- jmclient/__init__.py | 1 + jmclient/schedule.py | 32 ++++++++++++++++ scripts/joinmarketd.py | 16 +++++++- scripts/sample-schedule-for-testnet | 2 + scripts/sendpayment.py | 58 +++++++++++++++++++++-------- scripts/wallets/.gitignore | 4 ++ 6 files changed, 97 insertions(+), 16 deletions(-) create mode 100644 jmclient/schedule.py create mode 100644 scripts/sample-schedule-for-testnet create mode 100644 scripts/wallets/.gitignore 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