from commontest import DummyBlockchainInterface
import jmbitcoin as bitcoin
import binascii
import os
import copy
import random
import shutil
import pytest
import json
import struct
from base64 import b64encode
from typing import Optional
from jmbase import utxostr_to_utxo, hextobin
from jmclient import load_test_config, jm_single, set_commitment_file,\
    get_commitment_file, LegacyWallet, Taker, VolatileStorage,\
    get_network, WalletService, NO_ROUNDING, NotEnoughFundsException,\
    BTC_P2SH_P2WPKH, BTC_P2PKH, BTC_P2WPKH
from taker_test_data import t_utxos_by_mixdepth, t_orderbook,\
    t_maker_response, t_chosen_orders, t_dummy_ext
from commontest import default_max_cj_fee

pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")


def convert_utxos(utxodict):
    return_dict = {}
    for uk, val in utxodict.items():
        return_dict[utxostr_to_utxo(uk)[1]] = val
    return return_dict

class DummyWallet(LegacyWallet):
    def __init__(self):
        storage = VolatileStorage()
        super().initialize(storage, get_network(), max_mixdepth=5)
        super().__init__(storage)
        self._add_utxos()
        self.ex_utxos = {}
        self.inject_addr_get_failure = False

    def _add_utxos(self):
        for md, utxo in t_utxos_by_mixdepth.items():
            for i, (txid, data) in enumerate(utxo.items()):
                txid, index = txid.split(':')
                path = (b'dummy', md, i)
                self._utxos.add_utxo(binascii.unhexlify(txid), int(index),
                                     path, data['value'], md, 1)
                script = self._ENGINE.address_to_script(data['address'])
                self._script_map[script] = path

    def add_extra_utxo(self, txid, index, value, md,
                       script=None,
                       i=0):
        # note branch and index, path will be ignored in these test cases,
        # the tree is not real.
        # if we have extra utxos that have been added for some test,
        # we will need to return a script and an address, although it
        # won't be used; note we can't use base class get_utxos_by_mixdepth
        # because the paths are fake.
        if not script:
            address = "mrcNu71ztWjAQA6ww9kHiW3zBWSQidHXTQ"
            script = self._ENGINE.address_to_script(address)
        else:
            address = bitcoin.CCoinAddress.from_scriptPubKey(script)
        if md not in self.ex_utxos:
            self.ex_utxos[md] = {}
        self.ex_utxos[md].update({(txid, index): {"mixdepth": md,
                            "address": address,
                            "value": value,
                            "script": script,
                            "path": (b'dummy', md, i)}})

    def remove_extra_utxo(self, txid, index, md):
        del self.ex_utxos[(txid, index)]

    def get_utxos_by_mixdepth(self, include_disabled: bool = False,
                              verbose: bool = True,
                              includeheight: bool = False,
                              limit_mixdepth: Optional[int] = None):
        # utxostr conversion routines because taker_test_data uses hex:
        retval = {}
        for mixdepth, v in t_utxos_by_mixdepth.items():
            if not limit_mixdepth or limit_mixdepth == mixdepth:
                retval[mixdepth] = {}
                for i, (utxo, val) in enumerate(v.items()):
                    retval[mixdepth][utxostr_to_utxo(utxo)[1]] = val
                    val["script"] = self._ENGINE.address_to_script(
                        val['address'])
                    val["path"] = (b'dummy', mixdepth, i)
        for md, u in self.ex_utxos.items():
            retval[md].update(u)
        return retval

    def select_utxos(self, mixdepth, amount, utxo_filter=None, select_fn=None,
                     maxheight=None, includeaddr=False,
                     require_auth_address=False):
        if amount > self.get_balance_by_mixdepth()[mixdepth]:
            raise NotEnoughFundsException(amount, self.get_balance_by_mixdepth()[mixdepth])
        # comment as for get_utxos_by_mixdepth:
        retval = {}
        for k, v in t_utxos_by_mixdepth[mixdepth].items():
            success, u = utxostr_to_utxo(k)
            assert success
            retval[u] = v
            retval[u]["script"] = self.addr_to_script(retval[u]["address"])
        return retval

    def get_internal_addr(self, mixing_depth, bci=None):
        if self.inject_addr_get_failure:
            raise Exception("address get failure")
        return "mxeLuX8PP7qLkcM8uarHmdZyvP1b5e1Ynf"

    def sign_tx(self, tx, addrs):
        print("Pretending to sign on addresses: " + str(addrs))
        return True, None

    def sign(self, tx, i, priv, amount):
        """Sign a transaction; the amount field
        triggers the segwit style signing.
        """
        print("About to sign for this amount: " + str(amount))
        return tx

    def get_txtype(self):
        """Return string defining wallet type
        for purposes of transaction size estimates
        """
        return 'p2wpkh'

    def _get_key_from_path(self, path,
            validate_cache: bool = False):
        if path[0] == b'dummy':
            return struct.pack(b'B', path[2] + 1)*32 + b'\x01', self._ENGINE
        raise NotImplementedError()

    def get_key_from_addr(self, addr):
        """usable addresses: privkey all 1s, 2s, 3s, ... :"""
        privs = [x*32 + b"\x01" for x in [struct.pack(b'B', y) for y in range(1,6)]]
        addrs = {}
        """
        mrcNu71ztWjAQA6ww9kHiW3zBWSQidHXTQ
        n31WD8pkfAjg2APV78GnbDTdZb1QonBi5D
        mmVEKH61BZbLbnVEmk9VmojreB4G4PmBPd
        msxyyydNXTiBmt3SushXbH5Qh2ukBAThk3
        musGZczug3BAbqobmYherywCwL9REgNaNm
        """
        for p in privs:
            addrs[p] = BTC_P2PKH.privkey_to_address(p)
        for p, a in addrs.items():
            if a == addr:
                return p
        raise ValueError("No such keypair")

    def get_path_repr(self, path):
        return '/'.join(map(str, path))

    def is_standard_wallet_script(self, path):
        if path[0] == "nonstandard_path":
            return False
        return True

    def script_to_addr(self, script,
            validate_cache: bool = False):
        if self.script_to_path(script)[0] == "nonstandard_path":
            return "dummyaddr"
        return super().script_to_addr(script,
            validate_cache=validate_cache)


def dummy_order_chooser():
    return t_chosen_orders

def taker_finished(res, fromtx=False, waittime=0, txdetails=None):
    print("called taker finished callback")

def dummy_filter_orderbook(orders_fees, cjamount):
    print("calling dummy filter orderbook")
    return True

def get_taker(schedule=None, schedule_len=0, on_finished=None,
              filter_orders=None, custom_change=None):
    if not schedule:
        #note, for taker.initalize() this will result in junk
        schedule = [['a', 'b', 'c', 'd', 'e', 'f']]*schedule_len
    print("Using schedule: " + str(schedule))
    on_finished_callback = on_finished if on_finished else taker_finished
    filter_orders_callback = filter_orders if filter_orders else dummy_filter_orderbook
    taker = Taker(WalletService(DummyWallet()), schedule, default_max_cj_fee,
                  callbacks=[filter_orders_callback, None, on_finished_callback],
                  custom_change_address=custom_change)
    taker.wallet_service.current_blockheight = 10**6
    return taker

def test_filter_rejection(setup_taker):
    def filter_orders_reject(orders_feesl, cjamount):
        print("calling filter orders rejection")
        return False
    taker = get_taker(filter_orders=filter_orders_reject)
    taker.schedule = [[0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING]]
    res = taker.initialize(t_orderbook, [])
    assert not res[0]
    taker = get_taker(filter_orders=filter_orders_reject)
    taker.schedule = [[0, 0, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING]]
    res = taker.initialize(t_orderbook, [])
    assert not res[0]

@pytest.mark.parametrize(
    "mixdepth, cjamt, failquery, external, expected_success, amtpercent, age, mixdepth_extras",
    [
        (0, 110000000, False, False, True, 0, 0, {}),
        (0, 110000000, True, False, True, 0, 0, {}),
        (0, 110000000, False, True, True, 0, 0, {}),
        # this will fail to source from mixdepth 1 just because 2 < 50% of 5.5:
        (1, 550000000, False, False, False, 50, 5, {}),
        # this must fail to source even though the size in mixdepth 0 is enough:
        (1, 550000000, False, False, False, 50, 5, {0: [600000000]}),
        # this should succeed in sourcing because even though there are 9 utxos
        # in mixdepth 0, one of them is more than 20% (the original 2BTC):
        (0, 900000000, False, False, True, 20, 5, {0:[100000000]*8}),
        # this case must fail since the utxos are all at 20 confs and too new:
        (0, 110000000, False, False, False, 20, 25, {}),
        # make the confs in the spending mixdepth insufficient, while those
        # in another mixdepth are OK; must fail:
        (0, 110000000, False, False, False, 20, 5, {"confchange": {0: 1}}),
        # add one timelock script in mixdepth 0, must succeed without
        # trying to use it as PoDLE:
        (0, 110000000, False, False, True, 20, 5, {"custom-script": {0: [44000000]}}),
        # add one timelock script in mixdepth 0, must fail because only
        # the timelocked UTXO is big enough:
        (0, 1110000000, False, False, False, 20, 5, {"custom-script": {0: [1000000000]}}),
    ])
def test_make_commitment(setup_taker, mixdepth, cjamt, failquery, external,
                         expected_success, amtpercent, age, mixdepth_extras):
    def clean_up():
        jm_single().config.set("POLICY", "taker_utxo_age", old_taker_utxo_age)
        jm_single().config.set("POLICY", "taker_utxo_amtpercent", old_taker_utxo_amtpercent)
        set_commitment_file(old_commitment_file)
        jm_single().bc_interface.setQUSFail(False)
        jm_single().bc_interface.reset_confs()
        os.remove('dummyext')
    old_commitment_file = get_commitment_file()
    with open('dummyext', 'wb') as f:
        f.write(json.dumps(t_dummy_ext, indent=4).encode('utf-8'))
    if external:
        set_commitment_file('dummyext')

    # define the appropriate podle acceptance parameters in the global config:
    old_taker_utxo_age = jm_single().config.get("POLICY", "taker_utxo_age")
    old_taker_utxo_amtpercent = jm_single().config.get("POLICY", "taker_utxo_amtpercent")
    if expected_success:
        # set to defaults for mainnet
        newtua = "5"
        newtuap = "20"
    else:
        newtua = str(age)
        newtuap = str(amtpercent)
        jm_single().config.set("POLICY", "taker_utxo_age", newtua)
        jm_single().config.set("POLICY", "taker_utxo_amtpercent", newtuap)

    taker = get_taker([(mixdepth, cjamt, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", NO_ROUNDING)])

    # modify or add any extra utxos for this run:
    for k, v in mixdepth_extras.items():
        if k == "confchange":
            for k2, v2 in v.items():
                # set the utxos in mixdepth k2 to have confs v2:
                cdict = taker.wallet_service.get_utxos_by_mixdepth()[k2]
                jm_single().bc_interface.set_confs({utxo: v2 for utxo in cdict.keys()})
        elif k == "custom-script":
            # note: this is inspired by fidelity bonds, and currently
            # uses scripts of that specific timelock type, but is really
            # only testing the general concept: that commitments must
            # not be made on any non-standard script type.
            for k2, v2 in v.items():
                priv = os.urandom(32) + b"\x01"
                tl = random.randrange(1430454400, 1430494400)
                script_inner = bitcoin.mk_freeze_script(
                    bitcoin.privkey_to_pubkey(priv), tl)
                script_outer = bitcoin.redeem_script_to_p2wsh_script(
                    script_inner)
                taker.wallet_service.wallet._script_map[
                    script_outer] = ("nonstandard_path",)
                taker.wallet_service.add_extra_utxo(os.urandom(32),
                                    0, v2, k2, script=script_outer)
        else:
            for value in v:
                taker.wallet_service.add_extra_utxo(
                    os.urandom(32), 0, value, k)

    taker.cjamount = cjamt
    taker.input_utxos = taker.wallet_service.get_utxos_by_mixdepth()[mixdepth]
    taker.mixdepth = mixdepth
    if failquery:
        jm_single().bc_interface.setQUSFail(True)
    comm, revelation, msg = taker.make_commitment()
    if expected_success and failquery:
        # for manual tests, show the error message:
        print("Failure case due to QUS fail: ")
        print("Erromsg: ", msg)
        assert not comm
    elif expected_success:
        assert comm, "podle was not generated but should have been."
    else:
        # in these cases we have set the podle acceptance
        # parameters such that our in-mixdepth utxos are not good
        # enough.
        # for manual tests, show the errormsg:
        print("Failure case, errormsg: ", msg)
        assert not comm, "podle was generated but should not have been."
    clean_up()

def test_not_found_maker_utxos(setup_taker):
    taker = get_taker([(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)])
    orderbook = copy.deepcopy(t_orderbook)
    res = taker.initialize(orderbook, [])
    taker.orderbook = copy.deepcopy(t_chosen_orders) #total_cjfee unaffected, all same
    maker_response = copy.deepcopy(t_maker_response)
    jm_single().bc_interface.setQUSFail(True)
    res = taker.receive_utxos(maker_response)
    assert not res[0]
    assert res[1] == "Not enough counterparties responded to fill, giving up"
    jm_single().bc_interface.setQUSFail(False)

def test_auth_pub_not_found(setup_taker):
    taker = get_taker([(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)])
    orderbook = copy.deepcopy(t_orderbook)
    res = taker.initialize(orderbook, [])
    taker.orderbook = copy.deepcopy(t_chosen_orders) #total_cjfee unaffected, all same
    maker_response = copy.deepcopy(t_maker_response)
    utxos = [utxostr_to_utxo(x)[1] for x in [
        "03243f4a659e278a1333f8308f6aaf32db4692ee7df0340202750fd6c09150f6:1",
        "498faa8b22534f3b443c6b0ce202f31e12f21668b4f0c7a005146808f250d4c3:0",
        "3f3ea820d706e08ad8dc1d2c392c98facb1b067ae4c671043ae9461057bd2a3c:1"]]
    fake_query_results = [{'value': 200000000,
                           'address': "mrKTGvFfYUEqk52qPKUroumZJcpjHLQ6pn",
                           'script': hextobin('76a914767c956efe6092a775fea39a06d1cac9aae956d788ac'),
                           'utxo': utxos[i],
                           'confirms': 20} for i in range(3)]
    jm_single().bc_interface.insert_fake_query_results(fake_query_results)
    res = taker.receive_utxos(maker_response)
    assert not res[0]
    assert res[1] == "Not enough counterparties responded to fill, giving up"
    jm_single().bc_interface.insert_fake_query_results(None)

@pytest.mark.parametrize(
    "schedule, highfee, toomuchcoins, minmakers, notauthed, ignored, nocommit",
    [
        ([(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)], False, False,
         2, False, None, None),
        ([(0, 0, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)], False, False,
         2, False, None, None), #sweep
        ([(0, 0.2, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)], False, False,
         2, False, None, None), #tumble style non-int amounts
        #edge case triggers that don't fail
        ([(0, 0, 4, "mxeLuX8PP7qLkcM8uarHmdZyvP1b5e1Ynf", 0, NO_ROUNDING)], False, False,
         2, False, None, None), #sweep rounding error case 1
        ([(0, 0, 4, "mteaYsGsLCL9a4cftZFTpGEWXNwZyDt5KS", 0, NO_ROUNDING)], False, False,
         2, False, None, None), #sweep rounding error case 2
        ([(0, 199856001, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)], False, False,
         2, False, None, None), #trigger sub dust change for taker
        #edge case triggers that do fail
        ([(0, 199857000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)], False, False,
         2, False, None, None), #trigger negative change
        ([(0, 199599800, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)], False, False,
         2, False, None, None), #trigger sub dust change for maker
        ([(0, 20000000, 3, "INTERNAL", 0, NO_ROUNDING)], True, False,
         2, False, None, None), #test high fee
        ([(0, 20000000, 3, "INTERNAL", 0, NO_ROUNDING)], False, False,
         7, False, None, None), #test not enough cp
        ([(0, 80000000, 3, "INTERNAL", 0, NO_ROUNDING)], False, False,
         2, False, None, "30000"), #test failed commit
        ([(0, 20000000, 3, "INTERNAL", 0, NO_ROUNDING)], False, False,
         2, True, None, None), #test unauthed response
        ([(0, 5000000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)], False, True,
         2, False, None, None), #test too much coins
        ([(0, 0, 5, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)], False, False,
         2, False, ["J659UPUSLLjHJpaB", "J65z23xdjxJjC7er", 0], None), #test inadequate for sweep
    ])
def test_taker_init(setup_taker, schedule, highfee, toomuchcoins, minmakers,
                    notauthed, ignored, nocommit):
    #these tests do not trigger utxo_retries
    oldtakerutxoretries = jm_single().config.get("POLICY", "taker_utxo_retries")
    oldtakerutxoamtpercent = jm_single().config.get("POLICY", "taker_utxo_amtpercent")
    oldtxfees = jm_single().config.get("POLICY", "tx_fees")
    oldmaxsweepfeechange = jm_single().config.get("POLICY", "max_sweep_fee_change")
    jm_single().config.set("POLICY", "taker_utxo_retries", "20")
    jm_single().config.set("POLICY", "tx_fees", "30000")
    def clean_up():
        jm_single().config.set("POLICY", "minimum_makers", oldminmakers)
        jm_single().config.set("POLICY", "taker_utxo_retries", oldtakerutxoretries)
        jm_single().config.set("POLICY", "taker_utxo_amtpercent", oldtakerutxoamtpercent)
        jm_single().config.set("POLICY", "tx_fees", oldtxfees)
        jm_single().config.set("POLICY", "max_sweep_fee_change", oldmaxsweepfeechange)
    oldminmakers = jm_single().config.get("POLICY", "minimum_makers")
    jm_single().config.set("POLICY", "minimum_makers", str(minmakers))
    jm_single().config.set("POLICY", "max_sweep_fee_change", "3.0")
    taker = get_taker(schedule)
    orderbook = copy.deepcopy(t_orderbook) 
    if highfee:
        for o in orderbook:
            #trigger high-fee warning; but reset in next step
            o['cjfee'] = '1.0'
    if ignored:
        taker.ignored_makers = ignored
    if nocommit:
        jm_single().config.set("POLICY", "taker_utxo_amtpercent", nocommit)
    if schedule[0][1] == 0.2:
        #triggers calc-ing amount based on a fraction
        jm_single().mincjamount = 50000000 #bigger than 40m = 0.2 * 200m
        res = taker.initialize(orderbook, [])
        assert res[0]
        assert res[1] == jm_single().mincjamount
        return clean_up()
    res = taker.initialize(orderbook, [])
    if toomuchcoins or ignored:
        assert not res[0]
        return clean_up()
    if nocommit:
        print(str(res))
        assert res[0] == "commitment-failure"
        return clean_up()
    taker.orderbook = copy.deepcopy(t_chosen_orders) #total_cjfee unaffected, all same
    maker_response = copy.deepcopy(t_maker_response)
    if notauthed:
        #Doctor one of the maker response data fields
        maker_response["J659UPUSLLjHJpaB"][1] = "xx" #the auth pub
    if schedule[0][1] == 199857000:
        #triggers negative change
        # ((10 + 31 * outs + 41 * ins)*4 + 109 * ins)/4. plug in 9 ins and 8 outs gives
        #tx size estimate = 872.25 bytes. Times 30 ~= 26167.5.
        #makers offer 3000 txfee, so we pay 23168, plus maker fees = 3*0.0002*200000000
        #roughly, gives required selected = amt + 120k+23k, hence the above =
        #2btc - 143k sats = 199857000 (tweaked because of aggressive coin selection)
        #simulate the effect of a maker giving us a lot more utxos
        taker.utxos["dummy_for_negative_change"] = [(struct.pack(b"B", a) *32, a+1) for a in range(7,12)]
        with pytest.raises(ValueError) as e_info:
            res = taker.receive_utxos(maker_response)
        return clean_up()
    if schedule[0][1] == 199856001:
        #our own change is greater than zero but less than dust
        #use the same edge case as for negative change, don't add dummy inputs
        #(because we need tx creation to complete), but trigger case by
        #bumping dust threshold
        jm_single().BITCOIN_DUST_THRESHOLD = 14000
        res = taker.receive_utxos(maker_response)
        #should have succeeded to build tx
        assert res[0]
        #change should be none
        assert not taker.my_change_addr
        return clean_up()        
    if schedule[0][1] == 199599800:
        #need to force negative fees to make this feasible
        for k, v in taker.orderbook.items():
            v['cjfee'] = '-0.002'
        #            change_amount = (total_input - self.cjamount -
        #                     self.orderbook[nick]['txfee'] + real_cjfee)
        #suppose change amount is 1000 (sub dust), then solve for x;
        #given that real_cjfee = -0.002*x
        #change = 200000000 - x - 1000 - 0.002*x
        #x*1.002 = 1999999000; x = 199599800
        res = taker.receive_utxos(maker_response)
        assert not res[0]
        assert res[1] == "Not enough counterparties responded to fill, giving up"
        return clean_up()
    if schedule[0][3] == "mxeLuX8PP7qLkcM8uarHmdZyvP1b5e1Ynf":
        #to trigger rounding error for sweep (change non-zero),
        #modify the total_input via the values in self.input_utxos;
        #the amount to trigger a small + satoshi change is found by trial-error.
        #TODO note this test is not adequate, because the code is not;
        #the code does not *DO* anything if a condition is unexpected.
        taker.input_utxos = copy.deepcopy(t_utxos_by_mixdepth)[0]
        for k,v in taker.input_utxos.items():
            v["value"] = int(0.999805228 * v["value"])
        res = taker.receive_utxos(maker_response)
        assert res[0]
        return clean_up()
    if schedule[0][3] == "mteaYsGsLCL9a4cftZFTpGEWXNwZyDt5KS":
        # as above, but small -ve change instead of +ve.
        taker.input_utxos = copy.deepcopy(t_utxos_by_mixdepth)[0]
        for k,v in taker.input_utxos.items():
            v["value"] = int(0.999805028 * v["value"])
        res = taker.receive_utxos(maker_response)
        assert res[0]
        return clean_up()

    res = taker.receive_utxos(maker_response)
    if minmakers != 2:
        assert not res[0]
        assert res[1] == "Not enough counterparties responded to fill, giving up"
        return clean_up()
        
    assert res[0]
    #re-calling will trigger "finished" code, since schedule is "complete".
    res = taker.initialize(orderbook, [])
    assert not res[0]

    #some exception cases: no coinjoin address, no change address:
    #donations not yet implemented:
    taker.my_cj_addr = None
    with pytest.raises(NotImplementedError) as e_info:
        taker.prepare_my_bitcoin_data()
    with pytest.raises(NotImplementedError) as e_info:
        a = taker.coinjoin_address()
    taker.wallet_service.wallet.inject_addr_get_failure = True
    taker.my_cj_addr = "dummy"
    taker.my_change_addr = None
    assert not taker.prepare_my_bitcoin_data()
    #clean up
    return clean_up()

def test_custom_change(setup_taker):
    # create three random custom change addresses, one of each
    # known type in Joinmarket.
    privs = [x*32 + b"\x01" for x in [struct.pack(b'B', y) for y in range(1,4)]]
    scripts = [a.key_to_script(i) for a, i in zip([BTC_P2PKH, BTC_P2SH_P2WPKH, BTC_P2WPKH], privs)]
    addrs = [a.privkey_to_address(i) for a, i in zip([BTC_P2PKH, BTC_P2SH_P2WPKH, BTC_P2WPKH], privs)]
    schedule = [(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0, NO_ROUNDING)]
    for script, addr in zip(scripts, addrs):
        taker = get_taker(schedule, custom_change=addr)
        orderbook = copy.deepcopy(t_orderbook)
        res = taker.initialize(orderbook, [])
        taker.orderbook = copy.deepcopy(t_chosen_orders)
        maker_response = copy.deepcopy(t_maker_response)
        res = taker.receive_utxos(maker_response)
        assert res[0]
        # ensure that the transaction created for signing has
        # the address we intended with the right amount:
        custom_change_found = False
        for out in taker.latest_tx.vout:
            # input utxo is 200M; amount is 20M; as per logs:
            # totalin=200000000
            # my_txfee=13650 <- this estimate ignores address type
            # makers_txfee=3000
            # cjfee_total=12000 => changevalue=179974350
            # note that there is a small variation in the size of
            # the transaction (a few bytes) for the different scriptPubKey
            # type, but this is currently ignored in coinjoins by the
            # Taker (not true for direct send operations), hence we get
            # the same value for each different output type.
            if out.scriptPubKey == script and out.nValue == 179974350:
                # must be only one
                assert not custom_change_found
                custom_change_found = True
        assert custom_change_found

@pytest.mark.parametrize(
    "schedule_len",
    [
        (7),
    ])
def test_unconfirm_confirm(setup_taker, schedule_len):
    """These functions are: do-nothing by default (unconfirm, for Taker),
    and merely update schedule index for confirm (useful for schedules/tumbles).
    This tests that the on_finished callback correctly reports the fromtx
    variable as "False" once the schedule is complete.
    The exception to the above is that the txd passed in must match
    self.latest_tx, so we use a dummy value here for that.
    """
    class DummyTx(object):
        pass
    test_unconfirm_confirm.txflag = True
    def finished_for_confirms(res, fromtx=False, waittime=0, txdetails=None):
        assert res #confirmed should always send true
        test_unconfirm_confirm.txflag = fromtx

    taker = get_taker(schedule_len=schedule_len, on_finished=finished_for_confirms)
    taker.latest_tx = DummyTx()
    taker.latest_tx.vout = "blah"
    fake_txd = DummyTx()
    fake_txd.vin = "foo"
    fake_txd.vout = "blah"
    taker.unconfirm_callback(fake_txd, "b")
    for i in range(schedule_len-1):
        taker.schedule_index += 1
        fromtx = taker.confirm_callback(fake_txd, "b", 1)
        assert test_unconfirm_confirm.txflag
    taker.schedule_index += 1
    fromtx = taker.confirm_callback(fake_txd, "b", 1)
    assert not test_unconfirm_confirm.txflag
    
@pytest.mark.parametrize(
    "dummyaddr, schedule",
    [
        ("mrcNu71ztWjAQA6ww9kHiW3zBWSQidHXTQ",
         [(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw", 0)])
    ])
def test_on_sig(setup_taker, dummyaddr, schedule):
    #plan: create a new transaction with known inputs and dummy outputs;
    #then, create a signature with various inputs, pass in in b64 to on_sig.
    #in order for it to verify, the DummyBlockchainInterface will have to 
    #return the right values in query_utxo_set
    utxos = [(struct.pack(b"B", x) * 32, 1) for x in range(5)]
    #create 2 privkey + utxos that are to be ours
    privs = [x*32 + b"\x01" for x in [struct.pack(b'B', y) for y in range(1,6)]]
    scripts = [BTC_P2PKH.key_to_script(privs[x]) for x in range(5)]
    addrs = [BTC_P2PKH.privkey_to_address(privs[x]) for x in range(5)]
    fake_query_results = [{'value': 200000000, 'utxo': utxos[x], 'address': addrs[x],
                           'script': scripts[x], 'confirms': 20} for x in range(5)]

    dbci = DummyBlockchainInterface()
    dbci.insert_fake_query_results(fake_query_results)
    jm_single().bc_interface = dbci
    #make a transaction with all the fake results above, and some outputs
    outs = [{'value': 100000000, 'address': dummyaddr},
            {'value': 899990000, 'address': dummyaddr}]
    tx = bitcoin.mktx(utxos, outs)
    # since tx will be updated as it is signed, unlike in real life
    # (where maker signing operation doesn't happen here), we'll create
    # a second copy without the signatures:
    tx2 = bitcoin.mktx(utxos, outs)

    #prepare the Taker with the right intermediate data
    taker = get_taker(schedule=schedule)
    taker.nonrespondants=["cp1", "cp2", "cp3"]
    taker.latest_tx = tx
    #my inputs are the first 2 utxos
    taker.input_utxos = {utxos[0]:
                        {'address': addrs[0],
                         'script': scripts[0],
                         'value': 200000000},
                        utxos[1]:
                        {'address': addrs[1],
                         'script': scripts[1],
                         'value': 200000000}}    
    taker.utxos = {None: utxos[:2], "cp1": [utxos[2]], "cp2": [utxos[3]], "cp3":[utxos[4]]}
    for i in range(2):
        # placeholders required for my inputs
        taker.latest_tx.vin[i].scriptSig = bitcoin.CScript(hextobin('deadbeef'))
        tx2.vin[i].scriptSig = bitcoin.CScript(hextobin('deadbeef'))
    #to prepare for my signing, need to mark cjaddr:
    taker.my_cj_addr = dummyaddr
    #make signatures for the last 3 fake utxos, considered as "not ours":
    sig, msg = bitcoin.sign(tx2, 2, privs[2])
    assert sig, "Failed to sign: " + msg
    sig3 = b64encode(tx2.vin[2].scriptSig)
    taker.on_sig("cp1", sig3)
    #try sending the same sig again; should be ignored
    taker.on_sig("cp1", sig3)
    sig, msg = bitcoin.sign(tx2, 3, privs[3])
    assert sig, "Failed to sign: " + msg
    sig4 = b64encode(tx2.vin[3].scriptSig)
    #try sending junk instead of cp2's correct sig
    assert not taker.on_sig("cp2", str("junk")), "incorrectly accepted junk signature"
    taker.on_sig("cp2", sig4)
    sig, msg = bitcoin.sign(tx2, 4, privs[4])
    assert sig, "Failed to sign: " + msg
    #Before completing with the final signature, which will trigger our own
    #signing, try with an injected failure of query utxo set, which should
    #prevent this signature being accepted.
    dbci.setQUSFail(True)
    sig5 = b64encode(tx2.vin[4].scriptSig)
    assert not taker.on_sig("cp3", sig5), "incorrectly accepted sig5"
    #allow it to succeed, and try again
    dbci.setQUSFail(False)
    #this should succeed and trigger the we-sign code
    taker.on_sig("cp3", sig5)

@pytest.mark.parametrize(
    "schedule",
    [
        ([(0, 20000000, 3, "mnsquzxrHXpFsZeL42qwbKdCP2y1esN3qw")]),
    ])
def test_auth_counterparty(setup_taker, schedule):
    taker = get_taker(schedule=schedule)
    first_maker_response = t_maker_response["J659UPUSLLjHJpaB"]
    utxo, auth_pub, cjaddr, changeaddr, sig, maker_pub = first_maker_response
    auth_pub_tweaked = auth_pub[:8] + auth_pub[6:8] + auth_pub[10:]
    sig_tweaked = sig[:8] + sig[6:8] + sig[10:]
    assert taker.auth_counterparty(sig, auth_pub, maker_pub)
    assert not taker.auth_counterparty(sig, auth_pub_tweaked, maker_pub)
    assert not taker.auth_counterparty(sig_tweaked, auth_pub, maker_pub)

@pytest.fixture(scope="module")
def setup_taker(request):
    def clean():
        from twisted.internet import reactor
        for dc in reactor.getDelayedCalls():
            dc.cancel()
    request.addfinalizer(clean)
    def cmtdatateardown():
        shutil.rmtree("cmtdata")
    request.addfinalizer(cmtdatateardown)
    if not os.path.exists("cmtdata"):
            os.makedirs("cmtdata")
    load_test_config()
    jm_single().bc_interface = DummyBlockchainInterface()
    jm_single().config.set("BLOCKCHAIN", "network", "testnet")
