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#[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#[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#[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 pub fn new_no_sm(inputs: Vec<ClientInput<I>>) -> Self {
101 if inputs.is_empty() {
102 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 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 pub fn new_no_sm(outputs: Vec<ClientOutput<O>>) -> Self {
267 if outputs.is_empty() {
268 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 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#[derive(Debug, Clone, PartialEq, Eq)]
428pub struct FeeQuoteRequest {
429 pub input_amount: Amounts,
431 pub output_amount: Amounts,
433 pub input_fee: Amounts,
435 pub output_fee: Amounts,
437}
438
439#[derive(Debug, Clone, PartialEq, Eq)]
453pub struct FeeQuote {
454 pub input: Amounts,
458 pub output: Amounts,
461 pub dust: Amounts,
463}
464
465impl FeeQuote {
466 pub const ZERO: Self = Self {
470 input: Amounts::ZERO,
471 output: Amounts::ZERO,
472 dust: Amounts::ZERO,
473 };
474
475 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
486pub 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 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 if gross_up(Amount::from_msats(lo_bound)).msats > balance.msats {
548 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 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 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 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 while lo < hi {
606 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
619async 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 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 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
767fn find_range_of_matching_items(arr: &[usize], item: usize) -> Option<RangeInclusive<u64>> {
770 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;