1use std::fmt;
2use std::ops::RangeInclusive;
3use std::sync::Arc;
4
5use bitcoin::key::Keypair;
6use bitcoin::secp256k1;
7use fedimint_core::core::{
8 DynInput, DynOutput, IInput, IOutput, IntoDynInstance, ModuleInstanceId,
9};
10use fedimint_core::encoding::{Decodable, Encodable};
11use fedimint_core::module::Amounts;
12use fedimint_core::task::{MaybeSend, MaybeSync};
13use fedimint_core::transaction::{Transaction, TransactionSignature};
14use fedimint_logging::LOG_CLIENT;
15use itertools::multiunzip;
16use rand::{CryptoRng, Rng, RngCore};
17use secp256k1::Secp256k1;
18use tracing::warn;
19
20use crate::module::{IdxRange, OutPointRange, StateGenerator};
21use crate::sm::{self, DynState};
22use crate::{
23 InstancelessDynClientInput, InstancelessDynClientInputBundle, InstancelessDynClientInputSM,
24 InstancelessDynClientOutput, InstancelessDynClientOutputBundle, InstancelessDynClientOutputSM,
25 states_add_instance, states_to_instanceless_dyn,
26};
27
28#[derive(Clone, Debug)]
29pub struct ClientInput<I = DynInput> {
30 pub input: I,
31 pub keys: Vec<Keypair>,
32 pub amounts: Amounts,
33}
34
35#[derive(Clone)]
36pub struct ClientInputSM<S = DynState> {
37 pub state_machines: StateGenerator<S>,
38}
39
40impl<S> fmt::Debug for ClientInputSM<S> {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 f.write_str("ClientInputSM")
43 }
44}
45
46#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
48pub enum NeverClientContext {}
49
50impl sm::Context for NeverClientContext {
51 const KIND: Option<fedimint_core::core::ModuleKind> = None;
52}
53
54#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
59pub enum NeverClientStateMachine {}
60
61impl IntoDynInstance for NeverClientStateMachine {
62 type DynType = DynState;
63
64 fn into_dyn(self, _instance_id: ModuleInstanceId) -> Self::DynType {
65 unreachable!()
66 }
67}
68impl sm::State for NeverClientStateMachine {
69 type ModuleContext = NeverClientContext;
70
71 fn transitions(
72 &self,
73 _context: &Self::ModuleContext,
74 _global_context: &crate::DynGlobalClientContext,
75 ) -> Vec<sm::StateTransition<Self>> {
76 unreachable!()
77 }
78
79 fn operation_id(&self) -> fedimint_core::core::OperationId {
80 unreachable!()
81 }
82}
83
84#[derive(Clone, Debug)]
88pub struct ClientInputBundle<I = DynInput, S = DynState> {
89 pub(crate) inputs: Vec<ClientInput<I>>,
90 pub(crate) sm_gens: Vec<ClientInputSM<S>>,
91}
92
93impl<I> ClientInputBundle<I, NeverClientStateMachine> {
94 pub fn new_no_sm(inputs: Vec<ClientInput<I>>) -> Self {
99 if inputs.is_empty() {
100 warn!(target: LOG_CLIENT, "Empty input bundle will be illegal in the future");
102 }
103 Self {
104 inputs,
105 sm_gens: vec![],
106 }
107 }
108}
109
110impl<I, S> ClientInputBundle<I, S>
111where
112 I: IInput + MaybeSend + MaybeSync + 'static,
113 S: sm::IState + MaybeSend + MaybeSync + 'static,
114{
115 pub fn new(inputs: Vec<ClientInput<I>>, sm_gens: Vec<ClientInputSM<S>>) -> Self {
116 Self { inputs, sm_gens }
117 }
118
119 pub fn sms(&self) -> &[ClientInputSM<S>] {
120 &self.sm_gens
121 }
122
123 pub fn into_instanceless(self) -> InstancelessDynClientInputBundle {
124 InstancelessDynClientInputBundle {
125 inputs: self
126 .inputs
127 .into_iter()
128 .map(|input| InstancelessDynClientInput {
129 input: Box::new(input.input),
130 keys: input.keys,
131 amounts: input.amounts,
132 })
133 .collect(),
134 sm_gens: self
135 .sm_gens
136 .into_iter()
137 .map(|input_sm| InstancelessDynClientInputSM {
138 state_machines: states_to_instanceless_dyn(input_sm.state_machines),
139 })
140 .collect(),
141 }
142 }
143}
144
145impl<I, S> ClientInputBundle<I, S> {
146 pub fn inputs(&self) -> &[ClientInput<I>] {
147 &self.inputs
148 }
149
150 pub fn is_empty(&self) -> bool {
151 self.inputs.is_empty()
153 }
154}
155
156impl<I> IntoDynInstance for ClientInput<I>
157where
158 I: IntoDynInstance<DynType = DynInput> + 'static,
159{
160 type DynType = ClientInput;
161
162 fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientInput {
163 ClientInput {
164 input: self.input.into_dyn(module_instance_id),
165 keys: self.keys,
166 amounts: self.amounts,
167 }
168 }
169}
170
171impl<S> IntoDynInstance for ClientInputSM<S>
172where
173 S: IntoDynInstance<DynType = DynState> + 'static,
174{
175 type DynType = ClientInputSM;
176
177 fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientInputSM {
178 ClientInputSM {
179 state_machines: state_gen_to_dyn(self.state_machines, module_instance_id),
180 }
181 }
182}
183
184impl<I, S> IntoDynInstance for ClientInputBundle<I, S>
185where
186 I: IntoDynInstance<DynType = DynInput> + 'static,
187 S: IntoDynInstance<DynType = DynState> + 'static,
188{
189 type DynType = ClientInputBundle;
190
191 fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientInputBundle {
192 ClientInputBundle {
193 inputs: self
194 .inputs
195 .into_iter()
196 .map(|input| input.into_dyn(module_instance_id))
197 .collect::<Vec<ClientInput>>(),
198
199 sm_gens: self
200 .sm_gens
201 .into_iter()
202 .map(|input_sm| input_sm.into_dyn(module_instance_id))
203 .collect::<Vec<ClientInputSM>>(),
204 }
205 }
206}
207
208impl IntoDynInstance for InstancelessDynClientInputBundle {
209 type DynType = ClientInputBundle;
210
211 fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientInputBundle {
212 ClientInputBundle {
213 inputs: self
214 .inputs
215 .into_iter()
216 .map(|input| ClientInput {
217 input: DynInput::from_parts(module_instance_id, input.input),
218 keys: input.keys,
219 amounts: input.amounts,
220 })
221 .collect::<Vec<ClientInput>>(),
222
223 sm_gens: self
224 .sm_gens
225 .into_iter()
226 .map(|input_sm| ClientInputSM {
227 state_machines: states_add_instance(
228 module_instance_id,
229 input_sm.state_machines,
230 ),
231 })
232 .collect::<Vec<ClientInputSM>>(),
233 }
234 }
235}
236
237#[derive(Clone, Debug)]
238pub struct ClientOutputBundle<O = DynOutput, S = DynState> {
239 pub(crate) outputs: Vec<ClientOutput<O>>,
240 pub(crate) sm_gens: Vec<ClientOutputSM<S>>,
241}
242
243#[derive(Clone, Debug)]
244pub struct ClientOutput<O = DynOutput> {
245 pub output: O,
246 pub amounts: Amounts,
247}
248
249#[derive(Clone)]
250pub struct ClientOutputSM<S = DynState> {
251 pub state_machines: StateGenerator<S>,
252}
253
254impl<S> fmt::Debug for ClientOutputSM<S> {
255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256 f.write_str("ClientOutputSM")
257 }
258}
259impl<O> ClientOutputBundle<O, NeverClientStateMachine> {
260 pub fn new_no_sm(outputs: Vec<ClientOutput<O>>) -> Self {
265 if outputs.is_empty() {
266 warn!(target: LOG_CLIENT, "Empty output bundle will be illegal in the future");
268 }
269 Self {
270 outputs,
271 sm_gens: vec![],
272 }
273 }
274}
275impl<O, S> ClientOutputBundle<O, S> {
276 pub fn outputs(&self) -> &[ClientOutput<O>] {
277 &self.outputs
278 }
279}
280
281impl<O, S> ClientOutputBundle<O, S>
282where
283 O: IOutput + MaybeSend + MaybeSync + 'static,
284 S: sm::IState + MaybeSend + MaybeSync + 'static,
285{
286 pub fn new(outputs: Vec<ClientOutput<O>>, sm_gens: Vec<ClientOutputSM<S>>) -> Self {
287 Self { outputs, sm_gens }
288 }
289
290 pub fn sms(&self) -> &[ClientOutputSM<S>] {
291 &self.sm_gens
292 }
293
294 pub fn with(mut self, other: Self) -> Self {
295 self.outputs.extend(other.outputs);
296 self.sm_gens.extend(other.sm_gens);
297 self
298 }
299
300 pub fn into_instanceless(self) -> InstancelessDynClientOutputBundle {
301 InstancelessDynClientOutputBundle {
302 outputs: self
303 .outputs
304 .into_iter()
305 .map(|output| InstancelessDynClientOutput {
306 output: Box::new(output.output),
307 amounts: output.amounts,
308 })
309 .collect(),
310 sm_gens: self
311 .sm_gens
312 .into_iter()
313 .map(|output_sm| InstancelessDynClientOutputSM {
314 state_machines: states_to_instanceless_dyn(output_sm.state_machines),
315 })
316 .collect(),
317 }
318 }
319}
320
321impl<O, S> ClientOutputBundle<O, S> {
322 pub fn is_empty(&self) -> bool {
323 self.outputs.is_empty()
325 }
326}
327
328impl<I, S> IntoDynInstance for ClientOutputBundle<I, S>
329where
330 I: IntoDynInstance<DynType = DynOutput> + 'static,
331 S: IntoDynInstance<DynType = DynState> + 'static,
332{
333 type DynType = ClientOutputBundle;
334
335 fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientOutputBundle {
336 ClientOutputBundle {
337 outputs: self
338 .outputs
339 .into_iter()
340 .map(|output| output.into_dyn(module_instance_id))
341 .collect::<Vec<ClientOutput>>(),
342
343 sm_gens: self
344 .sm_gens
345 .into_iter()
346 .map(|output_sm| output_sm.into_dyn(module_instance_id))
347 .collect::<Vec<ClientOutputSM>>(),
348 }
349 }
350}
351
352impl IntoDynInstance for InstancelessDynClientOutputBundle {
353 type DynType = ClientOutputBundle;
354
355 fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientOutputBundle {
356 ClientOutputBundle {
357 outputs: self
358 .outputs
359 .into_iter()
360 .map(|output| ClientOutput {
361 output: DynOutput::from_parts(module_instance_id, output.output),
362 amounts: output.amounts,
363 })
364 .collect::<Vec<ClientOutput>>(),
365
366 sm_gens: self
367 .sm_gens
368 .into_iter()
369 .map(|output_sm| ClientOutputSM {
370 state_machines: states_add_instance(
371 module_instance_id,
372 output_sm.state_machines,
373 ),
374 })
375 .collect::<Vec<ClientOutputSM>>(),
376 }
377 }
378}
379
380impl<I> IntoDynInstance for ClientOutput<I>
381where
382 I: IntoDynInstance<DynType = DynOutput> + 'static,
383{
384 type DynType = ClientOutput;
385
386 fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientOutput {
387 ClientOutput {
388 output: self.output.into_dyn(module_instance_id),
389 amounts: self.amounts,
390 }
391 }
392}
393
394impl<S> IntoDynInstance for ClientOutputSM<S>
395where
396 S: IntoDynInstance<DynType = DynState> + 'static,
397{
398 type DynType = ClientOutputSM;
399
400 fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientOutputSM {
401 ClientOutputSM {
402 state_machines: state_gen_to_dyn(self.state_machines, module_instance_id),
403 }
404 }
405}
406
407#[derive(Debug, Clone, PartialEq, Eq)]
426pub struct FeeQuoteRequest {
427 pub input_amount: Amounts,
429 pub output_amount: Amounts,
431 pub input_fee: Amounts,
433 pub output_fee: Amounts,
435}
436
437#[derive(Debug, Clone, PartialEq, Eq)]
451pub struct FeeQuote {
452 pub input: Amounts,
456 pub output: Amounts,
459 pub dust: Amounts,
461}
462
463impl FeeQuote {
464 pub const ZERO: Self = Self {
468 input: Amounts::ZERO,
469 output: Amounts::ZERO,
470 dust: Amounts::ZERO,
471 };
472
473 pub fn total(&self) -> Amounts {
476 self.input
477 .clone()
478 .checked_add(&self.output)
479 .and_then(|sum| sum.checked_add(&self.dust))
480 .expect("aggregate fee components cannot overflow an Amounts")
481 }
482}
483
484#[derive(Default, Clone, Debug)]
485pub struct TransactionBuilder {
486 inputs: Vec<ClientInputBundle>,
487 outputs: Vec<ClientOutputBundle>,
488}
489
490impl TransactionBuilder {
491 pub fn new() -> Self {
492 Self::default()
493 }
494
495 pub fn with_inputs(mut self, inputs: ClientInputBundle) -> Self {
496 self.inputs.push(inputs);
497 self
498 }
499
500 pub fn with_outputs(mut self, outputs: ClientOutputBundle) -> Self {
501 self.outputs.push(outputs);
502 self
503 }
504
505 pub fn build<C, R: RngCore + CryptoRng>(
506 self,
507 secp_ctx: &Secp256k1<C>,
508 mut rng: R,
509 ) -> (Transaction, Vec<DynState>)
510 where
511 C: secp256k1::Signing + secp256k1::Verification,
512 {
513 let (input_idx_to_bundle_idx, inputs, input_keys): (Vec<_>, Vec<_>, Vec<_>) = multiunzip(
518 self.inputs
519 .iter()
520 .enumerate()
521 .flat_map(|(bundle_idx, bundle)| {
522 bundle
523 .inputs
524 .iter()
525 .map(move |input| (bundle_idx, input.input.clone(), input.keys.clone()))
526 }),
527 );
528 let (output_idx_to_bundle_idx, outputs): (Vec<_>, Vec<_>) = multiunzip(
531 self.outputs
532 .iter()
533 .enumerate()
534 .flat_map(|(bundle_idx, bundle)| {
535 bundle
536 .outputs
537 .iter()
538 .map(move |output| (bundle_idx, output.output.clone()))
539 }),
540 );
541 let nonce: [u8; 8] = rng.r#gen();
542
543 let txid = Transaction::tx_hash_from_parts(&inputs, &outputs, nonce);
544 let msg = secp256k1::Message::from_digest_slice(&txid[..]).expect("txid has right length");
545
546 let signatures = input_keys
547 .iter()
548 .flatten()
549 .map(|keypair| secp_ctx.sign_schnorr(&msg, keypair))
550 .collect();
551
552 let transaction = Transaction {
553 inputs,
554 outputs,
555 nonce,
556 signatures: TransactionSignature::NaiveMultisig(signatures),
557 };
558
559 let input_states = self
560 .inputs
561 .into_iter()
562 .enumerate()
563 .filter(|(_, bundle)| !bundle.is_empty())
564 .flat_map(|(bundle_idx, bundle)| {
565 let input_idxs = find_range_of_matching_items(&input_idx_to_bundle_idx, bundle_idx)
566 .expect("Non empty bundles must always have a match");
567 bundle.sm_gens.into_iter().flat_map(move |sm| {
568 (sm.state_machines)(OutPointRange::new(
569 txid,
570 IdxRange::from_inclusive(input_idxs.clone()).expect("can't overflow"),
571 ))
572 })
573 });
574
575 let output_states = self
576 .outputs
577 .into_iter()
578 .enumerate()
579 .filter(|(_, bundle)| !bundle.is_empty())
580 .flat_map(|(bundle_idx, bundle)| {
581 let output_idxs =
582 find_range_of_matching_items(&output_idx_to_bundle_idx, bundle_idx)
583 .expect("Non empty bundles must always have a match");
584 bundle.sm_gens.into_iter().flat_map(move |sm| {
585 (sm.state_machines)(OutPointRange::new(
586 txid,
587 IdxRange::from_inclusive(output_idxs.clone())
588 .expect("can't possibly overflow"),
589 ))
590 })
591 });
592 (transaction, input_states.chain(output_states).collect())
593 }
594
595 pub fn inputs(&self) -> impl Iterator<Item = &ClientInput> {
596 self.inputs.iter().flat_map(|i| i.inputs.iter())
597 }
598
599 pub fn outputs(&self) -> impl Iterator<Item = &ClientOutput> {
600 self.outputs.iter().flat_map(|i| i.outputs.iter())
601 }
602}
603
604fn find_range_of_matching_items(arr: &[usize], item: usize) -> Option<RangeInclusive<u64>> {
607 debug_assert!(arr.windows(2).all(|w| w[0] <= w[1]));
609
610 arr.iter()
611 .enumerate()
612 .filter_map(|(arr_idx, arr_item)| (*arr_item == item).then_some(arr_idx as u64))
613 .fold(None, |cur: Option<(u64, u64)>, idx| {
614 Some(cur.map_or((idx, idx), |cur| (cur.0.min(idx), cur.1.max(idx))))
615 })
616 .map(|(start, end)| start..=end)
617}
618
619#[test]
620fn find_range_of_matching_items_sanity() {
621 assert_eq!(find_range_of_matching_items(&[0, 0], 0), Some(0..=1));
622 assert_eq!(find_range_of_matching_items(&[0, 0, 1], 0), Some(0..=1));
623 assert_eq!(find_range_of_matching_items(&[0, 0, 1], 1), Some(2..=2));
624 assert_eq!(find_range_of_matching_items(&[0, 0, 1], 2), None);
625 assert_eq!(find_range_of_matching_items(&[], 0), None);
626}
627
628fn state_gen_to_dyn<S>(
629 state_gen: StateGenerator<S>,
630 module_instance: ModuleInstanceId,
631) -> StateGenerator<DynState>
632where
633 S: IntoDynInstance<DynType = DynState> + 'static,
634{
635 Arc::new(move |out_point_range| {
636 let states = state_gen(out_point_range);
637 states
638 .into_iter()
639 .map(|state| state.into_dyn(module_instance))
640 .collect()
641 })
642}
643
644#[cfg(test)]
645mod tests;