Skip to main content

fedimint_client_module/transaction/
builder.rs

1use std::fmt;
2use std::future::Future;
3use std::ops::RangeInclusive;
4use std::sync::Arc;
5
6use bitcoin::key::Keypair;
7use bitcoin::secp256k1;
8use fedimint_core::Amount;
9use fedimint_core::core::{
10    DynInput, DynOutput, IInput, IOutput, IntoDynInstance, ModuleInstanceId,
11};
12use fedimint_core::encoding::{Decodable, Encodable};
13use fedimint_core::module::Amounts;
14use fedimint_core::task::{MaybeSend, MaybeSync};
15use fedimint_core::transaction::{Transaction, TransactionSignature};
16use fedimint_logging::LOG_CLIENT;
17use itertools::multiunzip;
18use rand::{CryptoRng, Rng, RngCore};
19use secp256k1::Secp256k1;
20use tracing::warn;
21
22use crate::module::{IdxRange, OutPointRange, StateGenerator};
23use crate::sm::{self, DynState};
24use crate::{
25    InstancelessDynClientInput, InstancelessDynClientInputBundle, InstancelessDynClientInputSM,
26    InstancelessDynClientOutput, InstancelessDynClientOutputBundle, InstancelessDynClientOutputSM,
27    states_add_instance, states_to_instanceless_dyn,
28};
29
30#[derive(Clone, Debug)]
31pub struct ClientInput<I = DynInput> {
32    pub input: I,
33    pub keys: Vec<Keypair>,
34    pub amounts: Amounts,
35}
36
37#[derive(Clone)]
38pub struct ClientInputSM<S = DynState> {
39    pub state_machines: StateGenerator<S>,
40}
41
42impl<S> fmt::Debug for ClientInputSM<S> {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        f.write_str("ClientInputSM")
45    }
46}
47
48/// A fake [`sm::Context`] for [`NeverClientStateMachine`]
49#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
50pub enum NeverClientContext {}
51
52impl sm::Context for NeverClientContext {
53    const KIND: Option<fedimint_core::core::ModuleKind> = None;
54}
55
56/// A fake [`sm::State`] that can actually never happen.
57///
58/// Useful as a default for type inference in cases where there are no
59/// state machines involved in [`ClientInputBundle`].
60#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
61pub enum NeverClientStateMachine {}
62
63impl IntoDynInstance for NeverClientStateMachine {
64    type DynType = DynState;
65
66    fn into_dyn(self, _instance_id: ModuleInstanceId) -> Self::DynType {
67        unreachable!()
68    }
69}
70impl sm::State for NeverClientStateMachine {
71    type ModuleContext = NeverClientContext;
72
73    fn transitions(
74        &self,
75        _context: &Self::ModuleContext,
76        _global_context: &crate::DynGlobalClientContext,
77    ) -> Vec<sm::StateTransition<Self>> {
78        unreachable!()
79    }
80
81    fn operation_id(&self) -> fedimint_core::core::OperationId {
82        unreachable!()
83    }
84}
85
86/// A group of inputs and state machines responsible for driving their state
87///
88/// These must be kept together as a whole when including in a transaction.
89#[derive(Clone, Debug)]
90pub struct ClientInputBundle<I = DynInput, S = DynState> {
91    pub(crate) inputs: Vec<ClientInput<I>>,
92    pub(crate) sm_gens: Vec<ClientInputSM<S>>,
93}
94
95impl<I> ClientInputBundle<I, NeverClientStateMachine> {
96    /// A version of [`Self::new`] for times where input does not require any
97    /// state machines
98    ///
99    /// This avoids type inference issues of `S`, and saves some typing.
100    pub fn new_no_sm(inputs: Vec<ClientInput<I>>) -> Self {
101        if inputs.is_empty() {
102            // TODO: Make it return Result or assert?
103            warn!(target: LOG_CLIENT, "Empty input bundle will be illegal in the future");
104        }
105        Self {
106            inputs,
107            sm_gens: vec![],
108        }
109    }
110}
111
112impl<I, S> ClientInputBundle<I, S>
113where
114    I: IInput + MaybeSend + MaybeSync + 'static,
115    S: sm::IState + MaybeSend + MaybeSync + 'static,
116{
117    pub fn new(inputs: Vec<ClientInput<I>>, sm_gens: Vec<ClientInputSM<S>>) -> Self {
118        Self { inputs, sm_gens }
119    }
120
121    pub fn sms(&self) -> &[ClientInputSM<S>] {
122        &self.sm_gens
123    }
124
125    pub fn into_instanceless(self) -> InstancelessDynClientInputBundle {
126        InstancelessDynClientInputBundle {
127            inputs: self
128                .inputs
129                .into_iter()
130                .map(|input| InstancelessDynClientInput {
131                    input: Box::new(input.input),
132                    keys: input.keys,
133                    amounts: input.amounts,
134                })
135                .collect(),
136            sm_gens: self
137                .sm_gens
138                .into_iter()
139                .map(|input_sm| InstancelessDynClientInputSM {
140                    state_machines: states_to_instanceless_dyn(input_sm.state_machines),
141                })
142                .collect(),
143        }
144    }
145}
146
147impl<I, S> ClientInputBundle<I, S> {
148    pub fn inputs(&self) -> &[ClientInput<I>] {
149        &self.inputs
150    }
151
152    pub fn is_empty(&self) -> bool {
153        // Notably, sm_gen will not be called when inputs are empty anyway
154        self.inputs.is_empty()
155    }
156}
157
158impl<I> IntoDynInstance for ClientInput<I>
159where
160    I: IntoDynInstance<DynType = DynInput> + 'static,
161{
162    type DynType = ClientInput;
163
164    fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientInput {
165        ClientInput {
166            input: self.input.into_dyn(module_instance_id),
167            keys: self.keys,
168            amounts: self.amounts,
169        }
170    }
171}
172
173impl<S> IntoDynInstance for ClientInputSM<S>
174where
175    S: IntoDynInstance<DynType = DynState> + 'static,
176{
177    type DynType = ClientInputSM;
178
179    fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientInputSM {
180        ClientInputSM {
181            state_machines: state_gen_to_dyn(self.state_machines, module_instance_id),
182        }
183    }
184}
185
186impl<I, S> IntoDynInstance for ClientInputBundle<I, S>
187where
188    I: IntoDynInstance<DynType = DynInput> + 'static,
189    S: IntoDynInstance<DynType = DynState> + 'static,
190{
191    type DynType = ClientInputBundle;
192
193    fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientInputBundle {
194        ClientInputBundle {
195            inputs: self
196                .inputs
197                .into_iter()
198                .map(|input| input.into_dyn(module_instance_id))
199                .collect::<Vec<ClientInput>>(),
200
201            sm_gens: self
202                .sm_gens
203                .into_iter()
204                .map(|input_sm| input_sm.into_dyn(module_instance_id))
205                .collect::<Vec<ClientInputSM>>(),
206        }
207    }
208}
209
210impl IntoDynInstance for InstancelessDynClientInputBundle {
211    type DynType = ClientInputBundle;
212
213    fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientInputBundle {
214        ClientInputBundle {
215            inputs: self
216                .inputs
217                .into_iter()
218                .map(|input| ClientInput {
219                    input: DynInput::from_parts(module_instance_id, input.input),
220                    keys: input.keys,
221                    amounts: input.amounts,
222                })
223                .collect::<Vec<ClientInput>>(),
224
225            sm_gens: self
226                .sm_gens
227                .into_iter()
228                .map(|input_sm| ClientInputSM {
229                    state_machines: states_add_instance(
230                        module_instance_id,
231                        input_sm.state_machines,
232                    ),
233                })
234                .collect::<Vec<ClientInputSM>>(),
235        }
236    }
237}
238
239#[derive(Clone, Debug)]
240pub struct ClientOutputBundle<O = DynOutput, S = DynState> {
241    pub(crate) outputs: Vec<ClientOutput<O>>,
242    pub(crate) sm_gens: Vec<ClientOutputSM<S>>,
243}
244
245#[derive(Clone, Debug)]
246pub struct ClientOutput<O = DynOutput> {
247    pub output: O,
248    pub amounts: Amounts,
249}
250
251#[derive(Clone)]
252pub struct ClientOutputSM<S = DynState> {
253    pub state_machines: StateGenerator<S>,
254}
255
256impl<S> fmt::Debug for ClientOutputSM<S> {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        f.write_str("ClientOutputSM")
259    }
260}
261impl<O> ClientOutputBundle<O, NeverClientStateMachine> {
262    /// A version of [`Self::new`] for times where output does not require any
263    /// state machines
264    ///
265    /// This avoids type inference issues of `S`, and saves some typing.
266    pub fn new_no_sm(outputs: Vec<ClientOutput<O>>) -> Self {
267        if outputs.is_empty() {
268            // TODO: Make it return Result or assert?
269            warn!(target: LOG_CLIENT, "Empty output bundle will be illegal in the future");
270        }
271        Self {
272            outputs,
273            sm_gens: vec![],
274        }
275    }
276}
277impl<O, S> ClientOutputBundle<O, S> {
278    pub fn outputs(&self) -> &[ClientOutput<O>] {
279        &self.outputs
280    }
281}
282
283impl<O, S> ClientOutputBundle<O, S>
284where
285    O: IOutput + MaybeSend + MaybeSync + 'static,
286    S: sm::IState + MaybeSend + MaybeSync + 'static,
287{
288    pub fn new(outputs: Vec<ClientOutput<O>>, sm_gens: Vec<ClientOutputSM<S>>) -> Self {
289        Self { outputs, sm_gens }
290    }
291
292    pub fn sms(&self) -> &[ClientOutputSM<S>] {
293        &self.sm_gens
294    }
295
296    pub fn with(mut self, other: Self) -> Self {
297        self.outputs.extend(other.outputs);
298        self.sm_gens.extend(other.sm_gens);
299        self
300    }
301
302    pub fn into_instanceless(self) -> InstancelessDynClientOutputBundle {
303        InstancelessDynClientOutputBundle {
304            outputs: self
305                .outputs
306                .into_iter()
307                .map(|output| InstancelessDynClientOutput {
308                    output: Box::new(output.output),
309                    amounts: output.amounts,
310                })
311                .collect(),
312            sm_gens: self
313                .sm_gens
314                .into_iter()
315                .map(|output_sm| InstancelessDynClientOutputSM {
316                    state_machines: states_to_instanceless_dyn(output_sm.state_machines),
317                })
318                .collect(),
319        }
320    }
321}
322
323impl<O, S> ClientOutputBundle<O, S> {
324    pub fn is_empty(&self) -> bool {
325        // Notably, sm_gen will not be called when outputs are empty anyway
326        self.outputs.is_empty()
327    }
328}
329
330impl<I, S> IntoDynInstance for ClientOutputBundle<I, S>
331where
332    I: IntoDynInstance<DynType = DynOutput> + 'static,
333    S: IntoDynInstance<DynType = DynState> + 'static,
334{
335    type DynType = ClientOutputBundle;
336
337    fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientOutputBundle {
338        ClientOutputBundle {
339            outputs: self
340                .outputs
341                .into_iter()
342                .map(|output| output.into_dyn(module_instance_id))
343                .collect::<Vec<ClientOutput>>(),
344
345            sm_gens: self
346                .sm_gens
347                .into_iter()
348                .map(|output_sm| output_sm.into_dyn(module_instance_id))
349                .collect::<Vec<ClientOutputSM>>(),
350        }
351    }
352}
353
354impl IntoDynInstance for InstancelessDynClientOutputBundle {
355    type DynType = ClientOutputBundle;
356
357    fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientOutputBundle {
358        ClientOutputBundle {
359            outputs: self
360                .outputs
361                .into_iter()
362                .map(|output| ClientOutput {
363                    output: DynOutput::from_parts(module_instance_id, output.output),
364                    amounts: output.amounts,
365                })
366                .collect::<Vec<ClientOutput>>(),
367
368            sm_gens: self
369                .sm_gens
370                .into_iter()
371                .map(|output_sm| ClientOutputSM {
372                    state_machines: states_add_instance(
373                        module_instance_id,
374                        output_sm.state_machines,
375                    ),
376                })
377                .collect::<Vec<ClientOutputSM>>(),
378        }
379    }
380}
381
382impl<I> IntoDynInstance for ClientOutput<I>
383where
384    I: IntoDynInstance<DynType = DynOutput> + 'static,
385{
386    type DynType = ClientOutput;
387
388    fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientOutput {
389        ClientOutput {
390            output: self.output.into_dyn(module_instance_id),
391            amounts: self.amounts,
392        }
393    }
394}
395
396impl<S> IntoDynInstance for ClientOutputSM<S>
397where
398    S: IntoDynInstance<DynType = DynState> + 'static,
399{
400    type DynType = ClientOutputSM;
401
402    fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientOutputSM {
403        ClientOutputSM {
404            state_machines: state_gen_to_dyn(self.state_machines, module_instance_id),
405        }
406    }
407}
408
409/// The explicit (operation-supplied) side of a transaction, summarized for a
410/// fee quote via `Client::fee_quote`.
411///
412/// A module describes the inputs and outputs its operation would contribute by
413/// their gross value and federation fees (per unit), rather than building the
414/// actual transaction. The primary module for each affected unit then balances
415/// the resulting imbalance with change, and the full breakdown is returned as a
416/// [`FeeQuote`]. This avoids fabricating real inputs/outputs (which may require
417/// cryptographic material not available when quoting), since fees depend only
418/// on amounts and module.
419///
420/// All four fields are multi-unit [`Amounts`], so an operation can describe
421/// explicit items spanning several units (e.g. Bitcoin plus a custom currency);
422/// each unit is then balanced independently by its own primary module, the same
423/// way `Client::finalize_transaction` does.
424///
425/// For a typical receive there are no explicit outputs, so `output_amount` and
426/// `output_fee` are [`Amounts::ZERO`].
427#[derive(Debug, Clone, PartialEq, Eq)]
428pub struct FeeQuoteRequest {
429    /// Gross value of the operation's explicit inputs, per unit.
430    pub input_amount: Amounts,
431    /// Gross value of the operation's explicit outputs, per unit.
432    pub output_amount: Amounts,
433    /// Federation fees charged on the operation's explicit inputs, per unit.
434    pub input_fee: Amounts,
435    /// Federation fees charged on the operation's explicit outputs, per unit.
436    pub output_fee: Amounts,
437}
438
439/// Breakdown of the fee finalizing a transaction would incur, as computed by
440/// `Client::fee_quote` (a dry-run of the same balancing the real submission
441/// performs).
442///
443/// This is module-agnostic: the explicit inputs/outputs are described by the
444/// [`FeeQuoteRequest`] from whichever module is quoting (mint, lightning,
445/// wallet, …) and the change is generated by the primary module. The quote is
446/// point-in-time: it depends on the client's current inventory and can move as
447/// funds change.
448///
449/// Each field is a multi-unit [`Amounts`], since the quoted operation may span
450/// several units. The total fee is the sum of the breakdown fields, available
451/// via [`FeeQuote::total`].
452#[derive(Debug, Clone, PartialEq, Eq)]
453pub struct FeeQuote {
454    /// Federation fees charged on the spent (input) items — both the
455    /// transaction's explicit inputs and any inputs the primary module pulls in
456    /// to balance it.
457    pub input: Amounts,
458    /// Federation fees charged on the created (output) items — both the
459    /// transaction's explicit outputs and the change minted to balance it.
460    pub output: Amounts,
461    /// Sub-denomination remainder that cannot form an output and is lost.
462    pub dust: Amounts,
463}
464
465impl FeeQuote {
466    /// A zero fee, for operations that incur no cost at all — e.g. an ecash
467    /// send served entirely from existing exact-change notes, which submits
468    /// no transaction.
469    pub const ZERO: Self = Self {
470        input: Amounts::ZERO,
471        output: Amounts::ZERO,
472        dust: Amounts::ZERO,
473    };
474
475    /// Total fee per unit: everything the gross input value does not become a
476    /// net wallet gain. Equal to `input + output + dust`.
477    pub fn total(&self) -> Amounts {
478        self.input
479            .clone()
480            .checked_add(&self.output)
481            .and_then(|sum| sum.checked_add(&self.dust))
482            .expect("aggregate fee components cannot overflow an Amounts")
483    }
484}
485
486/// Finds the largest send amount payable in full out of `balance` when the
487/// payment carries both an *external* per-amount fee (e.g. a Lightning gateway
488/// routing fee) and the on-federation transaction fee.
489///
490/// Sending an amount `x` requires funding a value `gross_up(x)` on the
491/// federation — the external fee grosses the amount up, e.g. the outgoing
492/// Lightning contract is `x + gateway_fee(x)` — and funding that value costs an
493/// additional federation fee reported by `fee_quote` (the module's output fee
494/// plus the primary module's funding input fees, change output fees and dust).
495/// This returns the largest `x` in `min_amount..=max_amount` satisfying
496///
497/// ```text
498/// gross_up(x) + fee_quote(gross_up(x)).total().get_bitcoin() <= balance
499/// ```
500///
501/// or `None` if even `min_amount` is unaffordable.
502///
503/// The cost is not a closed form of `x`: the federation fee is charged per
504/// note, so note selection, denomination rounding, change and dust move it in
505/// steps as `x` crosses thresholds. Rather than invert an analytic formula this
506/// evaluates the real quote via a monotonic binary search — each step a single,
507/// non-committing `fee_quote` dry-run over the current inventory — and only
508/// ever advances the lower bound to a verified-affordable amount, so the result
509/// never overestimates. A `fee_quote` error (e.g. the balance cannot fund a
510/// value that large) is treated as unaffordable.
511///
512/// To keep those dry-runs cheap the search is *seeded near the top*: a first
513/// pass binary-searches `gross_up` alone (pure arithmetic, no quotes) for the
514/// largest fundable amount, peeling off the gateway fee for free, so the real
515/// fee quotes only probe the small window the federation fee leaves — a handful
516/// of dry-runs rather than one per bit of the balance.
517///
518/// The LNv2 and LNv1 send-all flows share this solver; they differ only in
519/// `gross_up` (the gateway fee model) and which `fee_quote` they pass.
520pub async fn max_affordable_send_amount<GrossUp, Quote, Fut>(
521    balance: Amount,
522    min_amount: Amount,
523    max_amount: Amount,
524    gross_up: GrossUp,
525    fee_quote: Quote,
526) -> Option<Amount>
527where
528    GrossUp: Fn(Amount) -> Amount,
529    Quote: Fn(Amount) -> Fut,
530    Fut: Future<Output = anyhow::Result<FeeQuote>>,
531{
532    // Nothing above the balance can ever be funded, so cap the upper bound.
533    let hi_bound = max_amount.msats.min(balance.msats);
534    let lo_bound = min_amount.msats;
535
536    if lo_bound > hi_bound {
537        return None;
538    }
539
540    // The maximum is never near the bottom of `[lo_bound, hi_bound]`: it sits
541    // just below the largest amount the balance can *fund*, short by only the
542    // federation fee. `gross_up` is a pure, cheap function (no fee quote), so
543    // binary-search it for free to find that fundable ceiling and seed the real
544    // (fee-quoting) search there. This peels off the gateway fee — usually the
545    // larger of the two — for free, leaving the expensive quotes to probe only
546    // the small window the federation fee opens up, instead of the whole balance.
547    if gross_up(Amount::from_msats(lo_bound)).msats > balance.msats {
548        // The balance can't even fund the smallest amount's gross-up.
549        return None;
550    }
551    let fundable_max = {
552        let mut lo = lo_bound;
553        let mut hi = hi_bound;
554        while lo < hi {
555            let mid = lo + (hi - lo).div_ceil(2);
556            if gross_up(Amount::from_msats(mid)).msats <= balance.msats {
557                lo = mid;
558            } else {
559                hi = mid - 1;
560            }
561        }
562        lo
563    };
564
565    let mut lo = lo_bound;
566    let mut hi = fundable_max;
567    let mut lo_affordable = false;
568
569    // Probe the fee once at `fundable_max`. The send overhead (the gross-up plus
570    // the federation fee, less the amount) is monotone non-decreasing, so the
571    // overhead here is an upper bound on the overhead at the true maximum, and
572    // `balance - overhead` is therefore a proven-affordable, tight lower bound.
573    // If the quote errors — real note selection can't fund a value this large —
574    // the seed is skipped and the search falls back to the full bracket below.
575    let funded = gross_up(Amount::from_msats(fundable_max));
576    if let Ok(quote) = fee_quote(funded).await {
577        let total = funded
578            .msats
579            .saturating_add(quote.total().get_bitcoin().msats);
580        if total <= balance.msats {
581            // Even the largest fundable amount fits once the fee is included.
582            return Some(Amount::from_msats(fundable_max));
583        }
584        let overhead = total.saturating_sub(fundable_max);
585        let seed = balance
586            .msats
587            .saturating_sub(overhead)
588            .clamp(lo_bound, fundable_max);
589        if send_amount_affordable(Amount::from_msats(seed), balance, &gross_up, &fee_quote).await {
590            lo = seed;
591            lo_affordable = true;
592        }
593    }
594
595    // Without a usable seed the search must still start from a verified
596    // affordable lower bound, or give up if even `lo_bound` is unaffordable.
597    if !lo_affordable
598        && !send_amount_affordable(Amount::from_msats(lo_bound), balance, &gross_up, &fee_quote)
599            .await
600    {
601        return None;
602    }
603
604    // Exact maximum within the (now tight) `[lo, hi]` bracket.
605    while lo < hi {
606        // Bias the midpoint up so the search converges toward `hi`.
607        let mid = lo + (hi - lo).div_ceil(2);
608
609        if send_amount_affordable(Amount::from_msats(mid), balance, &gross_up, &fee_quote).await {
610            lo = mid;
611        } else {
612            hi = mid - 1;
613        }
614    }
615
616    Some(Amount::from_msats(lo))
617}
618
619/// Whether sending `amount` is payable in full out of `balance`: the funded
620/// value `gross_up(amount)` plus its federation `fee_quote` must fit within the
621/// balance. A quote error (the balance cannot fund a value this large) counts
622/// as unaffordable, making this safe as the monotone predicate for
623/// [`max_affordable_send_amount`].
624async fn send_amount_affordable<GrossUp, Quote, Fut>(
625    amount: Amount,
626    balance: Amount,
627    gross_up: &GrossUp,
628    fee_quote: &Quote,
629) -> bool
630where
631    GrossUp: Fn(Amount) -> Amount,
632    Quote: Fn(Amount) -> Fut,
633    Fut: Future<Output = anyhow::Result<FeeQuote>>,
634{
635    let funded_amount = gross_up(amount);
636
637    if funded_amount > balance {
638        return false;
639    }
640
641    match fee_quote(funded_amount).await {
642        Ok(quote) => funded_amount + quote.total().get_bitcoin() <= balance,
643        Err(_) => false,
644    }
645}
646
647#[derive(Default, Clone, Debug)]
648pub struct TransactionBuilder {
649    inputs: Vec<ClientInputBundle>,
650    outputs: Vec<ClientOutputBundle>,
651}
652
653impl TransactionBuilder {
654    pub fn new() -> Self {
655        Self::default()
656    }
657
658    pub fn with_inputs(mut self, inputs: ClientInputBundle) -> Self {
659        self.inputs.push(inputs);
660        self
661    }
662
663    pub fn with_outputs(mut self, outputs: ClientOutputBundle) -> Self {
664        self.outputs.push(outputs);
665        self
666    }
667
668    pub fn build<C, R: RngCore + CryptoRng>(
669        self,
670        secp_ctx: &Secp256k1<C>,
671        mut rng: R,
672    ) -> (Transaction, Vec<DynState>)
673    where
674        C: secp256k1::Signing + secp256k1::Verification,
675    {
676        // `input_idx_to_bundle_idx[input_idx]` stores the index of a bundle the input
677        // at `input_idx` comes from, so we can call state machines of the
678        // corresponding bundle for every input bundle. It is always
679        // monotonically increasing, e.g. `[0, 0, 1, 2, 2, 2, 4]`
680        let (input_idx_to_bundle_idx, inputs, input_keys): (Vec<_>, Vec<_>, Vec<_>) = multiunzip(
681            self.inputs
682                .iter()
683                .enumerate()
684                .flat_map(|(bundle_idx, bundle)| {
685                    bundle
686                        .inputs
687                        .iter()
688                        .map(move |input| (bundle_idx, input.input.clone(), input.keys.clone()))
689                }),
690        );
691        // `output_idx_to_bundle` works exactly like `input_idx_to_bundle_idx` above,
692        // but for outputs.
693        let (output_idx_to_bundle_idx, outputs): (Vec<_>, Vec<_>) = multiunzip(
694            self.outputs
695                .iter()
696                .enumerate()
697                .flat_map(|(bundle_idx, bundle)| {
698                    bundle
699                        .outputs
700                        .iter()
701                        .map(move |output| (bundle_idx, output.output.clone()))
702                }),
703        );
704        let nonce: [u8; 8] = rng.r#gen();
705
706        let txid = Transaction::tx_hash_from_parts(&inputs, &outputs, nonce);
707        let msg = secp256k1::Message::from_digest_slice(&txid[..]).expect("txid has right length");
708
709        let signatures = input_keys
710            .iter()
711            .flatten()
712            .map(|keypair| secp_ctx.sign_schnorr(&msg, keypair))
713            .collect();
714
715        let transaction = Transaction {
716            inputs,
717            outputs,
718            nonce,
719            signatures: TransactionSignature::NaiveMultisig(signatures),
720        };
721
722        let input_states = self
723            .inputs
724            .into_iter()
725            .enumerate()
726            .filter(|(_, bundle)| !bundle.is_empty())
727            .flat_map(|(bundle_idx, bundle)| {
728                let input_idxs = find_range_of_matching_items(&input_idx_to_bundle_idx, bundle_idx)
729                    .expect("Non empty bundles must always have a match");
730                bundle.sm_gens.into_iter().flat_map(move |sm| {
731                    (sm.state_machines)(OutPointRange::new(
732                        txid,
733                        IdxRange::from_inclusive(input_idxs.clone()).expect("can't overflow"),
734                    ))
735                })
736            });
737
738        let output_states = self
739            .outputs
740            .into_iter()
741            .enumerate()
742            .filter(|(_, bundle)| !bundle.is_empty())
743            .flat_map(|(bundle_idx, bundle)| {
744                let output_idxs =
745                    find_range_of_matching_items(&output_idx_to_bundle_idx, bundle_idx)
746                        .expect("Non empty bundles must always have a match");
747                bundle.sm_gens.into_iter().flat_map(move |sm| {
748                    (sm.state_machines)(OutPointRange::new(
749                        txid,
750                        IdxRange::from_inclusive(output_idxs.clone())
751                            .expect("can't possibly overflow"),
752                    ))
753                })
754            });
755        (transaction, input_states.chain(output_states).collect())
756    }
757
758    pub fn inputs(&self) -> impl Iterator<Item = &ClientInput> {
759        self.inputs.iter().flat_map(|i| i.inputs.iter())
760    }
761
762    pub fn outputs(&self) -> impl Iterator<Item = &ClientOutput> {
763        self.outputs.iter().flat_map(|i| i.outputs.iter())
764    }
765}
766
767/// Find the range of indexes in an monotonically increasing `arr`, that is
768/// equal to `item`
769fn find_range_of_matching_items(arr: &[usize], item: usize) -> Option<RangeInclusive<u64>> {
770    // `arr` must be monotonically increasing
771    debug_assert!(arr.windows(2).all(|w| w[0] <= w[1]));
772
773    arr.iter()
774        .enumerate()
775        .filter_map(|(arr_idx, arr_item)| (*arr_item == item).then_some(arr_idx as u64))
776        .fold(None, |cur: Option<(u64, u64)>, idx| {
777            Some(cur.map_or((idx, idx), |cur| (cur.0.min(idx), cur.1.max(idx))))
778        })
779        .map(|(start, end)| start..=end)
780}
781
782#[test]
783fn find_range_of_matching_items_sanity() {
784    assert_eq!(find_range_of_matching_items(&[0, 0], 0), Some(0..=1));
785    assert_eq!(find_range_of_matching_items(&[0, 0, 1], 0), Some(0..=1));
786    assert_eq!(find_range_of_matching_items(&[0, 0, 1], 1), Some(2..=2));
787    assert_eq!(find_range_of_matching_items(&[0, 0, 1], 2), None);
788    assert_eq!(find_range_of_matching_items(&[], 0), None);
789}
790
791fn state_gen_to_dyn<S>(
792    state_gen: StateGenerator<S>,
793    module_instance: ModuleInstanceId,
794) -> StateGenerator<DynState>
795where
796    S: IntoDynInstance<DynType = DynState> + 'static,
797{
798    Arc::new(move |out_point_range| {
799        let states = state_gen(out_point_range);
800        states
801            .into_iter()
802            .map(|state| state.into_dyn(module_instance))
803            .collect()
804    })
805}
806
807#[cfg(test)]
808mod tests;