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