From 7dccb815ff866dc860dc2c913ffff3af45c232e8 Mon Sep 17 00:00:00 2001 From: Adam Gibson Date: Wed, 23 Nov 2016 21:48:20 +0200 Subject: [PATCH] Taker now schedules a sequence of transactions --- jmclient/__init__.py | 3 +- jmclient/client_protocol.py | 2 +- jmclient/taker.py | 71 ++++++++++++++++++++++++++----------- scripts/sendpayment.py | 56 +++++++++++++++-------------- 4 files changed, 82 insertions(+), 50 deletions(-) diff --git a/jmclient/__init__.py b/jmclient/__init__.py index eaedc55..0b9b16f 100644 --- a/jmclient/__init__.py +++ b/jmclient/__init__.py @@ -20,7 +20,8 @@ from .wallet import AbstractWallet, BitcoinCoreInterface, Wallet, \ from .configure import load_program_config, jm_single, get_p2pk_vbyte, \ get_network, jm_single, get_network, validate_address, get_irc_mchannels, \ check_utxo_blacklist -from .blockchaininterface import BlockrInterface, BlockchainInterface +from .blockchaininterface import (BlockrInterface, BlockchainInterface, + RegtestBitcoinCoreInterface, BitcoinCoreInterface) from .client_protocol import JMTakerClientProtocolFactory, start_reactor from .podle import set_commitment_file, get_commitment_file from .commands import * diff --git a/jmclient/client_protocol.py b/jmclient/client_protocol.py index 958c8a9..4e7b004 100644 --- a/jmclient/client_protocol.py +++ b/jmclient/client_protocol.py @@ -149,7 +149,7 @@ class JMTakerClientProtocol(amp.AMP): #True, self.cjamount, commitment, revelation, self.filtered_orderbook) if not retval[0]: jlog.info("Taker not continuing after receipt of orderbook") - return + return {'accepted': True} amt, cmt, rev, foffers = retval[1:] d = self.callRemote(commands.JMFill, amount=amt, diff --git a/jmclient/taker.py b/jmclient/taker.py index e9b76e7..d8693ea 100644 --- a/jmclient/taker.py +++ b/jmclient/taker.py @@ -26,37 +26,39 @@ class Taker(object): def __init__(self, wallet, - mixdepth, - amount, - n_counterparties, + schedule, order_chooser=weighted_order_choose, - external_addr=None, sign_method=None, callbacks=None): + """Schedule must be a list of tuples: [(mixdepth,cjamount,N, destaddr),..] + which will be a sequence of joins to do. + callbacks: + 1.filter orders callback: called to allow the client to decide whether + to accept the proposed offers. + 2.taker info callback: called to allow the client to read updates + 3.on finished callback: called on completion, either of the whole schedule + or early if a transactoin fails. + """ self.wallet = wallet - self.mixdepth = mixdepth - self.cjamount = amount - self.my_cj_addr = external_addr + self.schedule = schedule self.order_chooser = order_chooser - self.n_counterparties = n_counterparties self.ignored_makers = None - self.outputs = [] - self.cjfee_total = 0 - self.maker_txfee_contributions = 0 - self.txfee_default = 5000 - self.txid = None + self.schedule_index = -1 #allow custom wallet-based clients to use their own signing code; #currently only setting "wallet" is allowed, calls wallet.sign_tx(tx) self.sign_method = sign_method #External callers can set any of the 3 callbacks for filtering orders, #sending info messages to client, and action on completion. if callbacks: - self.filter_orders_callback, self.taker_info_callback, self.on_finished_callback = callbacks + self.filter_orders_callback = callbacks[0] + self.taker_info_callback = callbacks[1] + self.on_finished_callback = callbacks[2] if not self.taker_info_callback: self.taker_info_callback = self.default_taker_info_callback if not self.on_finished_callback: self.on_finished_callback = self.default_on_finished_callback else: + #default settings; currently not possible, see default_on_finished self.filter_orders_callback = None self.taker_info_callback = self.default_taker_info_callback self.on_finished_callback = self.default_on_finished_callback @@ -64,16 +66,34 @@ class Taker(object): def default_taker_info_callback(self, infotype, msg): jlog.debug(infotype + ":" + msg) - def default_on_finished_callback(self, result): - jlog.debug("Taker default on finished callback: " + str(result)) + def default_on_finished_callback(self, result, fromtx=False): + """Currently not possible without access to the client protocol factory""" + raise NotImplementedError def initialize(self, orderbook): """Once the daemon is active and has returned the current orderbook, - select offers and prepare a commitment, then send it to the protocol - to fill offers. + select offers, re-initialize variables and prepare a commitment, + then send it to the protocol to fill offers. """ - #reset destinations - self.outputs = [] + #choose the next item in the schedule + self.schedule_index += 1 + if self.schedule_index == len(self.schedule): + jlog.debug("Finished all scheduled transactions") + self.on_finished_callback(True) + return (False,) + else: + #read the settings from the schedule entry + si = self.schedule[self.schedule_index] + self.mixdepth = si[0] + self.cjamount = si[1] + self.n_counterparties = si[2] + self.my_cj_addr = si[3] + self.outputs = [] + self.cjfee_total = 0 + self.maker_txfee_contributions = 0 + self.txfee_default = 5000 + self.txid = None + if not self.filter_orderbook(orderbook): return (False,) #choose coins to spend @@ -567,8 +587,17 @@ class Taker(object): self.txid = btc.txhash(tx) jlog.debug('txid = ' + self.txid) pushed = jm_single().bc_interface.pushtx(tx) - self.on_finished_callback(pushed) + jm_single().bc_interface.add_tx_notify( + self.latest_tx, self.unconfirm_callback, + self.confirm_callback, self.my_cj_addr) + self.on_finished_callback(pushed, fromtx=True) def self_sign_and_push(self): self.self_sign() return self.push() + + def unconfirm_callback(self, txd, txid): + jlog.debug("Unconfirmed callback in sendpayment, ignoring") + + def confirm_callback(self, txd, txid, confirmations): + jlog.debug("Confirmed callback in sendpayment, confs: " + str(confirmations)) diff --git a/scripts/sendpayment.py b/scripts/sendpayment.py index e953b7c..914f2fb 100644 --- a/scripts/sendpayment.py +++ b/scripts/sendpayment.py @@ -8,12 +8,17 @@ This is primitive and not yet well tested, but it is designed to illustrate the main functionality of the new architecture: this code can be run in a separate environment (but not safely over the internet, better on one machine) to the joinmarketdaemon. -Moreover, it can run several transactions using the -b option, e.g.: +Moreover, it can run several transactions as specified in a "schedule", like: -`python sendpayment.py -b 3 -N 3 -m 1 walletseed amount address`; +[(mixdepth, amount, N, destination),(m,a,N,d),..] + +call it like the normal Joinmarket sendpayment, but optionally add +a port for the daemon: + +`python sendpayment.py -p 12345 -N 3 -m 1 walletseed amount address`; + +TODO: schedule can be read from file. -note here only one destination address for multiple transactions, -only one mixdepth and other settings; this is just a proof of concept. The idea is that the "backend" (daemon) will keep its orderbook and stay connected on the message channel between runs, only shutting down after all are complete. @@ -48,7 +53,7 @@ from jmclient import (Taker, load_program_config, choose_orders, choose_sweep_orders, pick_order, cheapest_order_choose, weighted_order_choose, Wallet, BitcoinCoreWallet, - estimate_tx_fee) + RegtestBitcoinCoreInterface, estimate_tx_fee) from jmbase.support import get_log, debug_dump_object @@ -212,33 +217,30 @@ def main(): else: wallet = BitcoinCoreWallet(fromaccount=wallet_name) jm_single().bc_interface.sync_wallet(wallet) - def taker_finished(res): - global wallet - global txcount - txcount += 1 - if res: - log.debug("Transaction finished OK, result was: ") + + def taker_finished(res, fromtx=False): + if fromtx: + if res: + jm_single().bc_interface.sync_wallet(wallet) + clientfactory.getClient().clientStart() + else: + #a transaction failed; just stop + reactor.stop() else: - log.info("A transaction failed, quitting") - sys.exit(1) - if txcount > options.txcount: - log.debug("Shutting down") + if not res: + log.info("Did not complete successfully, shutting down") + log.info("All transactions completed correctly") reactor.stop() - else: - #need to update for new transactions; only working for - #regtest at the moment (otherwise too slow) - jm_single().bc_interface.sync_wallet(wallet) - time.sleep(3) #for blocks to mine - #restarts from the entry point of the client-server protocol (JMInit) - #with the *same* Taker object. - clientfactory.getClient().clientStart() + #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 taker = Taker(wallet, - options.mixdepth, - amount, - options.makercount, + schedule, order_chooser=chooseOrdersFunc, - external_addr=destaddr, callbacks=(None, None, taker_finished)) clientfactory = JMTakerClientProtocolFactory(taker) start_reactor("localhost", options.daemonport, clientfactory)