def direct_send(wallet_service,
                amount,
                mixdepth,
                destination,
                answeryes=False,
                accept_callback=None,
                info_callback=None,
                error_callback=None,
                return_transaction=False,
                with_final_psbt=False,
                optin_rbf=False,
                custom_change_addr=None):
    """Send coins directly from one mixdepth to one destination address;
    does not need IRC. Sweep as for normal sendpayment (set amount=0).
    If answeryes is True, callback/command line query is not performed.
    If optin_rbf is True, the nSequence values are changed as appropriate.
    If accept_callback is None, command line input for acceptance is assumed,
    else this callback is called:
    accept_callback:
    ====
    args:
    deserialized tx, destination address, amount in satoshis,
    fee in satoshis, custom change address

    returns:
    True if accepted, False if not
    ====
    info_callback and error_callback takes one parameter, the information
    message (when tx is pushed or error occured), and returns nothing.

    This function returns:
    1. False if there is any failure.
    2. The txid if transaction is pushed, and return_transaction is False,
       and with_final_psbt is False.
    3. The full CMutableTransaction if return_transaction is True and
       with_final_psbt is False.
    4. The PSBT object if with_final_psbt is True, and in
       this case the transaction is *NOT* broadcast.
    """
    #Sanity checks
    assert validate_address(destination)[0] or is_burn_destination(destination)
    assert custom_change_addr is None or validate_address(
        custom_change_addr)[0]
    assert amount > 0 or custom_change_addr is None
    assert isinstance(mixdepth, numbers.Integral)
    assert mixdepth >= 0
    assert isinstance(amount, numbers.Integral)
    assert amount >= 0
    assert isinstance(wallet_service.wallet, BaseWallet)

    if is_burn_destination(destination):
        #Additional checks
        if not isinstance(wallet_service.wallet, FidelityBondMixin):
            log.error("Only fidelity bond wallets can burn coins")
            return
        if answeryes:
            log.error(
                "Burning coins not allowed without asking for confirmation")
            return
        if mixdepth != FidelityBondMixin.FIDELITY_BOND_MIXDEPTH:
            log.error("Burning coins only allowed from mixdepth " +
                      str(FidelityBondMixin.FIDELITY_BOND_MIXDEPTH))
            return
        if amount != 0:
            log.error(
                "Only sweeping allowed when burning coins, to keep the tx " +
                "small. Tip: use the coin control feature to freeze utxos")
            return

    txtype = wallet_service.get_txtype()
    if amount == 0:
        #doing a sweep
        utxos = wallet_service.get_utxos_by_mixdepth()[mixdepth]
        if utxos == {}:
            log.error("There are no available utxos in mixdepth: " +
                      str(mixdepth) + ", quitting.")
            return
        total_inputs_val = sum([va['value'] for u, va in utxos.items()])

        if is_burn_destination(destination):
            if len(utxos) > 1:
                log.error(
                    "Only one input allowed when burning coins, to keep " +
                    "the tx small. Tip: use the coin control feature to freeze utxos"
                )
                return
            address_type = FidelityBondMixin.BIP32_BURN_ID
            index = wallet_service.wallet.get_next_unused_index(
                mixdepth, address_type)
            path = wallet_service.wallet.get_path(mixdepth, address_type,
                                                  index)
            privkey, engine = wallet_service.wallet._get_key_from_path(path)
            pubkey = engine.privkey_to_pubkey(privkey)
            pubkeyhash = Hash160(pubkey)

            #size of burn output is slightly different from regular outputs
            burn_script = mk_burn_script(pubkeyhash)
            fee_est = estimate_tx_fee(len(utxos),
                                      0,
                                      txtype=txtype,
                                      extra_bytes=len(burn_script) / 2)

            outs = [{
                "script": burn_script,
                "value": total_inputs_val - fee_est
            }]
            destination = "BURNER OUTPUT embedding pubkey at " \
                + wallet_service.wallet.get_path_repr(path) \
                + "\n\nWARNING: This transaction if broadcasted will PERMANENTLY DESTROY your bitcoins\n"
        else:
            #regular sweep (non-burn)
            fee_est = estimate_tx_fee(len(utxos), 1, txtype=txtype)
            outs = [{
                "address": destination,
                "value": total_inputs_val - fee_est
            }]
    else:
        #not doing a sweep; we will have change
        #8 inputs to be conservative
        initial_fee_est = estimate_tx_fee(8, 2, txtype=txtype)
        utxos = wallet_service.select_utxos(mixdepth, amount + initial_fee_est)
        if len(utxos) < 8:
            fee_est = estimate_tx_fee(len(utxos), 2, txtype=txtype)
        else:
            fee_est = initial_fee_est
        total_inputs_val = sum([va['value'] for u, va in utxos.items()])
        changeval = total_inputs_val - fee_est - amount
        outs = [{"value": amount, "address": destination}]
        change_addr = wallet_service.get_internal_addr(mixdepth) if custom_change_addr is None \
                      else custom_change_addr
        outs.append({"value": changeval, "address": change_addr})

    #compute transaction locktime, has special case for spending timelocked coins
    tx_locktime = compute_tx_locktime()
    if mixdepth == FidelityBondMixin.FIDELITY_BOND_MIXDEPTH and \
            isinstance(wallet_service.wallet, FidelityBondMixin):
        for outpoint, utxo in utxos.items():
            path = wallet_service.script_to_path(utxo["script"])
            if not FidelityBondMixin.is_timelocked_path(path):
                continue
            path_locktime = path[-1]
            tx_locktime = max(tx_locktime, path_locktime + 1)
            #compute_tx_locktime() gives a locktime in terms of block height
            #timelocked addresses use unix time instead
            #OP_CHECKLOCKTIMEVERIFY can only compare like with like, so we
            #must use unix time as the transaction locktime

    #Now ready to construct transaction
    log.info("Using a fee of: " + amount_to_str(fee_est) + ".")
    if amount != 0:
        log.info("Using a change value of: " + amount_to_str(changeval) + ".")
    tx = make_shuffled_tx(list(utxos.keys()), outs, 2, tx_locktime)

    if optin_rbf:
        for inp in tx.vin:
            inp.nSequence = 0xffffffff - 2

    inscripts = {}
    spent_outs = []
    for i, txinp in enumerate(tx.vin):
        u = (txinp.prevout.hash[::-1], txinp.prevout.n)
        inscripts[i] = (utxos[u]["script"], utxos[u]["value"])
        spent_outs.append(CMutableTxOut(utxos[u]["value"], utxos[u]["script"]))
    if with_final_psbt:
        # here we have the PSBTWalletMixin do the signing stage
        # for us:
        new_psbt = wallet_service.create_psbt_from_tx(tx,
                                                      spent_outs=spent_outs)
        serialized_psbt, err = wallet_service.sign_psbt(new_psbt.serialize())
        if err:
            log.error("Failed to sign PSBT, quitting. Error message: " + err)
            return False
        new_psbt_signed = PartiallySignedTransaction.deserialize(
            serialized_psbt)
        print("Completed PSBT created: ")
        print(wallet_service.human_readable_psbt(new_psbt_signed))
        return new_psbt_signed
    else:
        success, msg = wallet_service.sign_tx(tx, inscripts)
        if not success:
            log.error("Failed to sign transaction, quitting. Error msg: " +
                      msg)
            return
        log.info("Got signed transaction:\n")
        log.info(human_readable_transaction(tx))
        actual_amount = amount if amount != 0 else total_inputs_val - fee_est
        sending_info = "Sends: " + amount_to_str(actual_amount) + \
            " to destination: " + destination
        if custom_change_addr:
            sending_info += ", custom change to: " + custom_change_addr
        log.info(sending_info)
        if not answeryes:
            if not accept_callback:
                if input('Would you like to push to the network? (y/n):'
                         )[0] != 'y':
                    log.info(
                        "You chose not to broadcast the transaction, quitting."
                    )
                    return False
            else:
                accepted = accept_callback(human_readable_transaction(tx),
                                           destination, actual_amount, fee_est,
                                           custom_change_addr)
                if not accepted:
                    return False
        if jm_single().bc_interface.pushtx(tx.serialize()):
            txid = bintohex(tx.GetTxid()[::-1])
            successmsg = "Transaction sent: " + txid
            cb = log.info if not info_callback else info_callback
            cb(successmsg)
            txinfo = txid if not return_transaction else tx
            return txinfo
        else:
            errormsg = "Transaction broadcast failed!"
            cb = log.error if not error_callback else error_callback
            cb(errormsg)
            return False
Exemple #2
0
def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False,
                accept_callback=None, info_callback=None):
    """Send coins directly from one mixdepth to one destination address;
    does not need IRC. Sweep as for normal sendpayment (set amount=0).
    If answeryes is True, callback/command line query is not performed.
    If accept_callback is None, command line input for acceptance is assumed,
    else this callback is called:
    accept_callback:
    ====
    args:
    deserialized tx, destination address, amount in satoshis, fee in satoshis
    returns:
    True if accepted, False if not
    ====
    The info_callback takes one parameter, the information message (when tx is
    pushed), and returns nothing.

    This function returns:
    The txid if transaction is pushed, False otherwise
    """
    #Sanity checks
    assert validate_address(destination)[0] or is_burn_destination(destination)
    assert isinstance(mixdepth, numbers.Integral)
    assert mixdepth >= 0
    assert isinstance(amount, numbers.Integral)
    assert amount >=0
    assert isinstance(wallet_service.wallet, BaseWallet)

    if is_burn_destination(destination):
        #Additional checks
        if not isinstance(wallet_service.wallet, FidelityBondMixin):
            log.error("Only fidelity bond wallets can burn coins")
            return
        if answeryes:
            log.error("Burning coins not allowed without asking for confirmation")
            return
        if mixdepth != FidelityBondMixin.FIDELITY_BOND_MIXDEPTH:
            log.error("Burning coins only allowed from mixdepth " + str(
                FidelityBondMixin.FIDELITY_BOND_MIXDEPTH))
            return
        if amount != 0:
            log.error("Only sweeping allowed when burning coins, to keep the tx " +
                "small. Tip: use the coin control feature to freeze utxos")
            return

    from pprint import pformat
    txtype = wallet_service.get_txtype()
    if amount == 0:
        utxos = wallet_service.get_utxos_by_mixdepth()[mixdepth]
        if utxos == {}:
            log.error(
                "There are no available utxos in mixdepth: " + str(mixdepth) + ", quitting.")
            return

        total_inputs_val = sum([va['value'] for u, va in iteritems(utxos)])

        if is_burn_destination(destination):
            if len(utxos) > 1:
                log.error("Only one input allowed when burning coins, to keep "
                    + "the tx small. Tip: use the coin control feature to freeze utxos")
                return
            address_type = FidelityBondMixin.BIP32_BURN_ID
            index = wallet_service.wallet.get_next_unused_index(mixdepth, address_type)
            path = wallet_service.wallet.get_path(mixdepth, address_type, index)
            privkey, engine = wallet_service.wallet._get_key_from_path(path)
            pubkey = engine.privkey_to_pubkey(privkey)
            pubkeyhash = bin_hash160(pubkey)

            #size of burn output is slightly different from regular outputs
            burn_script = mk_burn_script(pubkeyhash) #in hex
            fee_est = estimate_tx_fee(len(utxos), 0, txtype=txtype, extra_bytes=len(burn_script)/2)

            outs = [{"script": burn_script, "value": total_inputs_val - fee_est}]
            destination = "BURNER OUTPUT embedding pubkey at " \
                + wallet_service.wallet.get_path_repr(path) \
                + "\n\nWARNING: This transaction if broadcasted will PERMANENTLY DESTROY your bitcoins\n"
        else:
            #regular send (non-burn)
            fee_est = estimate_tx_fee(len(utxos), 1, txtype=txtype)
            outs = [{"address": destination, "value": total_inputs_val - fee_est}]
    else:
        #8 inputs to be conservative
        initial_fee_est = estimate_tx_fee(8,2, txtype=txtype)
        utxos = wallet_service.select_utxos(mixdepth, amount + initial_fee_est)
        if len(utxos) < 8:
            fee_est = estimate_tx_fee(len(utxos), 2, txtype=txtype)
        else:
            fee_est = initial_fee_est
        total_inputs_val = sum([va['value'] for u, va in iteritems(utxos)])
        changeval = total_inputs_val - fee_est - amount
        outs = [{"value": amount, "address": destination}]
        change_addr = wallet_service.get_internal_addr(mixdepth)
        outs.append({"value": changeval, "address": change_addr})

    #compute transaction locktime, has special case for spending timelocked coins
    tx_locktime = compute_tx_locktime()
    if mixdepth == FidelityBondMixin.FIDELITY_BOND_MIXDEPTH and \
            isinstance(wallet_service.wallet, FidelityBondMixin):
        for outpoint, utxo in utxos.items():
            path = wallet_service.script_to_path(
                wallet_service.addr_to_script(utxo["address"]))
            if not FidelityBondMixin.is_timelocked_path(path):
                continue
            path_locktime = path[-1]
            tx_locktime = max(tx_locktime, path_locktime+1)
            #compute_tx_locktime() gives a locktime in terms of block height
            #timelocked addresses use unix time instead
            #OP_CHECKLOCKTIMEVERIFY can only compare like with like, so we
            #must use unix time as the transaction locktime

    #Now ready to construct transaction
    log.info("Using a fee of : " + amount_to_str(fee_est) + ".")
    if amount != 0:
        log.info("Using a change value of: " + amount_to_str(changeval) + ".")
    txsigned = sign_tx(wallet_service, make_shuffled_tx(
        list(utxos.keys()), outs, False, 2, tx_locktime), utxos)
    log.info("Got signed transaction:\n")
    log.info(pformat(txsigned))
    tx = serialize(txsigned)
    log.info("In serialized form (for copy-paste):")
    log.info(tx)
    actual_amount = amount if amount != 0 else total_inputs_val - fee_est
    log.info("Sends: " + amount_to_str(actual_amount) + " to destination: " + destination)
    if not answeryes:
        if not accept_callback:
            if input('Would you like to push to the network? (y/n):')[0] != 'y':
                log.info("You chose not to broadcast the transaction, quitting.")
                return False
        else:
            accepted = accept_callback(pformat(txsigned), destination, actual_amount,
                                       fee_est)
            if not accepted:
                return False
    jm_single().bc_interface.pushtx(tx)
    txid = txhash(tx)
    successmsg = "Transaction sent: " + txid
    cb = log.info if not info_callback else info_callback
    cb(successmsg)
    return txid