def make_tx(
            self, *,
            coins: Sequence[PartialTxInput],
            inputs: List[PartialTxInput],
            outputs: List[PartialTxOutput],
            change_addrs: Sequence[str],
            fee_estimator_vb: Callable,
            dust_threshold: int,
            BIP69_sort: bool = True,
    ) -> PartialTransaction:
        """Select unspent coins to spend to pay outputs.  If the change is
        greater than dust_threshold (after adding the change output to
        the transaction) it is kept, otherwise none is sent and it is
        added to the transaction fee.

        `inputs` and `outputs` are guaranteed to be a subset of the
        inputs and outputs of the resulting transaction.
        `coins` are further UTXOs we can choose from.

        Note: fee_estimator_vb expects virtual bytes
        """
        # Deterministic randomness from coins
        utxos = [c.prevout.serialize_to_network() for c in coins]
        self.p = PRNG(b''.join(sorted(utxos)))

        assert len(outputs) > 0 or len(change_addrs) == 1, \
            "sweeps with 0 outputs should not use multiple change addresses"

        # Copy the outputs so when adding change we don't modify "outputs"
        base_tx = PartialTransaction.from_io(inputs[:], outputs[:], BIP69_sort=BIP69_sort)
        input_value = base_tx.input_value()

        # Weight of the transaction with no inputs and no change
        # Note: this will use legacy tx serialization as the need for "segwit"
        # would be detected from inputs. The only side effect should be that the
        # marker and flag are excluded, which is compensated in get_tx_weight()
        # FIXME calculation will be off by this (2 wu) in case of RBF batching
        base_weight = base_tx.estimated_weight()
        # by setting spent_amount = dust_threshold if there are no outputs we ensure that
        # enough inputs are added so there is always at least a change output created
        # as txs have to have at least 1 output according to consensus rules
        spent_amount = base_tx.output_value() if outputs else dust_threshold

        def fee_estimator_w(weight):
            return fee_estimator_vb(Transaction.virtual_size_from_weight(weight))

        def sufficient_funds(buckets, *, bucket_value_sum):
            '''Given a list of buckets, return True if it has enough
            value to pay for the transaction'''
            # assert bucket_value_sum == sum(bucket.value for bucket in buckets)  # expensive!
            total_input = input_value + bucket_value_sum
            if total_input < spent_amount:  # shortcut for performance
                return False
            # any bitcoin tx must have at least 1 input by consensus
            # (check we add some new UTXOs now or already have some fixed inputs)
            if not buckets and not inputs:
                return False
            # note re performance: so far this was constant time
            # what follows is linear in len(buckets)
            total_weight = self._get_tx_weight(buckets, base_weight=base_weight)
            return total_input >= spent_amount + fee_estimator_w(total_weight)

        def tx_from_buckets(buckets):
            return self._construct_tx_from_selected_buckets(
                buckets=buckets,
                base_tx=base_tx,
                change_addrs=change_addrs,
                fee_estimator_w=fee_estimator_w,
                dust_threshold=dust_threshold,
                base_weight=base_weight,
                BIP69_sort=BIP69_sort,
            )
        # Collect the coins into buckets
        all_buckets = self.bucketize_coins(coins, fee_estimator_vb=fee_estimator_vb)
        # Filter some buckets out. Only keep those that have positive effective value.
        # Note that this filtering is intentionally done on the bucket level
        # instead of per-coin, as each bucket should be either fully spent or not at all.
        # (e.g. CoinChooserPrivacy ensures that same-address coins go into one bucket)
        all_buckets = list(filter(lambda b: b.effective_value > 0, all_buckets))
        # Choose a subset of the buckets
        scored_candidate = self.choose_buckets(all_buckets, sufficient_funds,
                                               self.penalty_func(base_tx, tx_from_buckets=tx_from_buckets))
        tx = scored_candidate.tx

        self.logger.info(f"using {len(tx.inputs())} inputs")
        self.logger.info(f"using buckets: {[bucket.desc for bucket in scored_candidate.buckets]}")

        return tx
Select unspent coins to spend to pay outputs.  If the change is
        greater than dust_threshold (after adding the change output to
        the transaction) it is kept, otherwise none is sent and it is
        added to the transaction fee.

        `inputs` and `outputs` are guaranteed to be a subset of the
        inputs and outputs of the resulting transaction.
        `coins` are further UTXOs we can choose from.

        Note: fee_estimator_vb expects virtual bytes
        
electrum/coinchooser.py