Skip to main content

smplx_sdk/transaction/
final_transaction.rs

1use std::collections::HashMap;
2
3use bitcoin_hashes::sha256;
4
5use simplicityhl::elements::pset::{Input, PartiallySignedTransaction};
6use simplicityhl::elements::{
7    AssetId, TxOutSecrets,
8    confidential::{AssetBlindingFactor, ValueBlindingFactor},
9};
10
11use crate::provider::SimplicityNetwork;
12use crate::utils;
13
14use super::partial_input::{IssuanceInput, PartialInput, ProgramInput, RequiredSignature};
15use super::partial_output::PartialOutput;
16
17/// Constant is defined for fee calculation on transaction sending.
18pub const WITNESS_SCALE_FACTOR: usize = 4;
19
20/// A structure representing the details of token issuance and related metadata.
21#[derive(Debug, Clone)]
22pub struct IssuanceDetails {
23    /// The unique `AssetId` generated from the provided entropy, representing the issued tokens struct.
24    pub asset_id: AssetId,
25    /// The `AssetId` corresponding to the reissuance (inflation) token, used for minting new tokens.
26    pub inflation_asset_id: AssetId,
27    /// The entropy value (`sha256::Midstate`) that was used to derive both the `asset_id` and `inflation_asset_id`.
28    pub asset_entropy: sha256::Midstate,
29}
30
31/// Represents the final input structure put into a `FinalTransaction` for processing.
32#[derive(Clone)]
33pub struct FinalInput {
34    /// Holds the base input data required for the operation.
35    pub partial_input: PartialInput,
36    /// Holds program inputs, which are used for program witness finalization.
37    pub program_input: Option<ProgramInput>,
38    /// Contains optional issuance-related information.
39    pub issuance_input: Option<IssuanceInput>,
40    /// Required signature for finalizing the transaction.
41    pub required_sig: RequiredSignature,
42}
43
44impl FinalInput {
45    /// Creates a new instance of the type with the specified `partial_input` and `required_sig`.
46    #[must_use]
47    pub fn new(partial_input: PartialInput, required_sig: RequiredSignature) -> Self {
48        Self {
49            partial_input,
50            required_sig,
51            program_input: None,
52            issuance_input: None,
53        }
54    }
55
56    /// Sets the `program_input` field with the given `ProgramInput` and returns the modified `FinalInput`.
57    #[must_use]
58    pub fn with_program(mut self, program_input: ProgramInput) -> Self {
59        self.program_input = Some(program_input);
60
61        self
62    }
63
64    /// Sets the `issuance_input` field of the current instance and returns the updated `FinalInput`.
65    #[must_use]
66    pub fn with_issuance(mut self, issuance_input: IssuanceInput) -> Self {
67        self.issuance_input = Some(issuance_input);
68
69        self
70    }
71
72    /// Retrieves the issuance details associated with the current instance.
73    ///
74    /// # Errors
75    ///
76    /// This method does not explicitly return errors but returns `None` if no issuance
77    /// input is available.
78    #[must_use]
79    pub fn get_issuance_details(&self) -> Option<IssuanceDetails> {
80        match &self.issuance_input {
81            Some(issuance_input) => {
82                let asset_entropy = match issuance_input {
83                    IssuanceInput::Issuance { asset_entropy, .. } => {
84                        utils::asset_entropy(&self.partial_input.outpoint(), *asset_entropy)
85                    }
86                    IssuanceInput::Reissuance { asset_entropy, .. } => {
87                        sha256::Midstate::from_byte_array(*asset_entropy)
88                    }
89                };
90
91                let asset_id = AssetId::from_entropy(asset_entropy);
92                let inflation_asset_id = AssetId::reissuance_token_from_entropy(asset_entropy, false);
93
94                Some(IssuanceDetails {
95                    asset_id,
96                    inflation_asset_id,
97                    asset_entropy,
98                })
99            }
100            None => None,
101        }
102    }
103
104    /// Converts the current object into an `Input` representation, including any
105    /// issuance input and partial input details.
106    ///
107    /// # Panics
108    ///
109    /// This function will panic if the `issuance_input` is of type `Reissuance`
110    ///  and the `partial_input.secrets` field is `None` or does not contain the necessary
111    ///  confidential information. Specifically, a panic occurs when attempting to unwrap the `asset_bf` value.
112    #[must_use]
113    pub fn to_input(&self) -> Input {
114        let mut pst_input = self.partial_input.to_input();
115
116        // populate the input manually since `input.merge` is private
117        if let Some(issuance_input) = &self.issuance_input {
118            let issue = issuance_input.to_input();
119
120            pst_input.issuance_value_amount = issue.issuance_value_amount;
121            pst_input.issuance_asset_entropy = issue.issuance_asset_entropy;
122            pst_input.issuance_inflation_keys = issue.issuance_inflation_keys;
123            pst_input.blinded_issuance = issue.blinded_issuance;
124
125            if matches!(issuance_input, IssuanceInput::Reissuance { .. }) {
126                let issuance_blinding_nonce = self
127                    .partial_input
128                    .secrets
129                    .expect("Reissuance input must be confidential")
130                    .asset_bf
131                    .into_inner();
132
133                pst_input.issuance_blinding_nonce = Some(issuance_blinding_nonce);
134            }
135        }
136
137        pst_input
138    }
139}
140
141/// A struct representing a final (but not yet signed) transaction.
142#[derive(Clone)]
143pub struct FinalTransaction {
144    inputs: Vec<FinalInput>,
145    outputs: Vec<PartialOutput>,
146}
147
148impl FinalTransaction {
149    /// Creates a new instance of the final transaction with default values.
150    #[must_use]
151    #[allow(clippy::new_without_default)]
152    pub fn new() -> Self {
153        Self {
154            inputs: Vec::new(),
155            outputs: Vec::new(),
156        }
157    }
158
159    /// Adds a new input to the transaction.
160    ///
161    /// # Panics
162    /// Panics if the requested signature is not `NativeEcdsa` or `None`.
163    /// (i.e. if `required_sig` is `RequiredSignature::Witness` or `RequiredSignature::WitnessWithPath`)
164    pub fn add_input(&mut self, partial_input: PartialInput, required_sig: RequiredSignature) {
165        match required_sig {
166            RequiredSignature::Witness(_) | RequiredSignature::WitnessWithPath(_, _) => {
167                panic!("Requested signature is not NativeEcdsa or None")
168            }
169            _ => {}
170        }
171
172        self.push_new_input(FinalInput::new(partial_input, required_sig));
173    }
174
175    /// Adds a new program input to the transaction.
176    ///
177    /// # Panics
178    /// The function will panic if the `required_sig` parameter is of type `RequiredSignature::NativeEcdsa`,
179    /// as this type of signature is not applicable for program inputs.
180    pub fn add_program_input(
181        &mut self,
182        partial_input: PartialInput,
183        program_input: ProgramInput,
184        required_sig: RequiredSignature,
185    ) {
186        if let RequiredSignature::NativeEcdsa = required_sig {
187            panic!("Requested signature is not Witness or None");
188        }
189
190        self.push_new_input(FinalInput::new(partial_input, required_sig).with_program(program_input));
191    }
192
193    /// Adds an issuance (or reissuance) input to the transaction.
194    ///
195    /// # Panics
196    /// This function panics if the `required_sig` is of type `Witness` or
197    /// `WitnessWithPath`, as these signature types are not allowed in the current context.
198    pub fn add_issuance_input(
199        &mut self,
200        partial_input: PartialInput,
201        issuance_input: IssuanceInput,
202        required_sig: RequiredSignature,
203    ) -> IssuanceDetails {
204        match required_sig {
205            RequiredSignature::Witness(_) | RequiredSignature::WitnessWithPath(_, _) => {
206                panic!("Requested signature is not NativeEcdsa or None")
207            }
208            _ => {}
209        }
210
211        self.push_new_input(FinalInput::new(partial_input, required_sig).with_issuance(issuance_input))
212            .unwrap()
213    }
214
215    /// Adds an issuance program input to the transaction with the specified parameters.
216    ///
217    /// # Panics
218    /// Panics if the `required_sig` parameter is of type `RequiredSignature::NativeEcdsa`.
219    /// Also panics if the populated input fails to return valid issuance details.
220    pub fn add_program_issuance_input(
221        &mut self,
222        partial_input: PartialInput,
223        program_input: ProgramInput,
224        issuance_input: IssuanceInput,
225        required_sig: RequiredSignature,
226    ) -> IssuanceDetails {
227        if let RequiredSignature::NativeEcdsa = required_sig {
228            panic!("Requested signature is not Witness or None");
229        }
230
231        self.push_new_input(
232            FinalInput::new(partial_input, required_sig)
233                .with_program(program_input)
234                .with_issuance(issuance_input),
235        )
236        .unwrap()
237    }
238
239    /// Removes an input from the list of inputs at the specified index.
240    pub fn remove_input(&mut self, index: usize) -> Option<FinalInput> {
241        if self.inputs.get(index).is_some() {
242            return Some(self.inputs.remove(index));
243        }
244
245        None
246    }
247
248    /// Adds a partial output to the list of outputs.
249    pub fn add_output(&mut self, partial_output: PartialOutput) {
250        self.outputs.push(partial_output);
251    }
252
253    /// Removes an output from the `outputs` list at the specified index.
254    ///
255    /// # Panics
256    /// This function does not panic. If the `index` is invalid, it will return `None` instead of causing a panic.
257    pub fn remove_output(&mut self, index: usize) -> Option<PartialOutput> {
258        if self.outputs.get(index).is_some() {
259            return Some(self.outputs.remove(index));
260        }
261
262        None
263    }
264
265    /// Provides a slice reference to the collection of `FinalInput` elements.
266    #[must_use]
267    pub fn inputs(&self) -> &[FinalInput] {
268        &self.inputs
269    }
270
271    /// Provides mutable access to the `inputs` field.
272    ///
273    /// This method returns a mutable slice of `FinalInput` elements,
274    /// allowing the caller to modify the elements in the `inputs` field.
275    pub fn inputs_mut(&mut self) -> &mut [FinalInput] {
276        &mut self.inputs
277    }
278
279    /// Returns a reference to the slice of `PartialOutput` elements contained within the struct.
280    #[must_use]
281    pub fn outputs(&self) -> &[PartialOutput] {
282        &self.outputs
283    }
284
285    /// Provides mutable access to the `outputs` field of the current struct.
286    pub fn outputs_mut(&mut self) -> &mut [PartialOutput] {
287        &mut self.outputs
288    }
289
290    /// Returns the number of inputs associated with the current instance.
291    #[must_use]
292    pub fn n_inputs(&self) -> usize {
293        self.inputs.len()
294    }
295
296    /// Returns the number of outputs associated with the object.
297    #[must_use]
298    pub fn n_outputs(&self) -> usize {
299        self.outputs.len()
300    }
301
302    /// Checks if any of the outputs require blinding, determines if at least one of them has a `blinding_key` specified.
303    #[must_use]
304    pub fn needs_blinding(&self) -> bool {
305        self.outputs.iter().any(|el| el.blinding_key.is_some())
306    }
307
308    /// Calculates the fee delta for a transaction based on the inputs and outputs.
309    ///
310    /// The fee delta represents the net difference between the available asset amount
311    /// from the transaction's inputs and the consumed asset amount by its outputs.
312    /// The function considers the network's policy asset to determine which inputs
313    /// and outputs contribute to the calculation.
314    ///
315    /// # Panics
316    /// Function will panic if the asset doesn't be unblinded correctly, and PST input asset and amount is confidential.
317    #[must_use]
318    pub fn calculate_fee_delta(&self, network: &SimplicityNetwork) -> i64 {
319        let mut available_amount = 0;
320
321        for input in &self.inputs {
322            match input.partial_input.secrets {
323                // this is an unblinded confidential input
324                Some(secrets) => {
325                    if secrets.asset == network.policy_asset() {
326                        available_amount += secrets.value;
327                    }
328                }
329                // this is an explicit input
330                None => {
331                    if input.partial_input.asset.unwrap() == network.policy_asset() {
332                        available_amount += input.partial_input.amount.unwrap();
333                    }
334                }
335            }
336        }
337
338        let consumed_amount = self
339            .outputs
340            .iter()
341            .filter(|output| output.asset == network.policy_asset())
342            .fold(0_u64, |acc, output| acc + output.amount);
343
344        available_amount.cast_signed() - consumed_amount.cast_signed()
345    }
346
347    /// Computes the transaction fee based on the provided weight and fee rate.
348    ///
349    /// Overall, the function calculates the virtual size (vsize) of the transaction as:
350    /// `weight / WITNESS_SCALE_FACTOR`, rounded up to the nearest whole number.
351    /// Then, the fee is computed as `(vsize * fee_rate / 1000.0)`, also rounded up.
352    ///
353    /// # Returns
354    /// The transaction fee in satoshis, rounded up to the nearest whole number.
355    #[allow(
356        clippy::cast_possible_truncation,
357        clippy::cast_precision_loss,
358        clippy::cast_sign_loss
359    )]
360    #[must_use]
361    pub fn calculate_fee(&self, weight: usize, fee_rate: f32) -> u64 {
362        let vsize = weight.div_ceil(WITNESS_SCALE_FACTOR);
363
364        (vsize as f32 * fee_rate / 1000.0).ceil() as u64
365    }
366
367    /// Extracts a partially signed transaction (PST) and a mapping of input secrets from the current state.
368    ///
369    /// # Panics
370    /// Function will panic if the pst input is a confidential issuance.
371    #[must_use]
372    pub fn extract_pst(&self) -> (PartiallySignedTransaction, HashMap<usize, TxOutSecrets>) {
373        let mut input_secrets = HashMap::new();
374        let mut pst = PartiallySignedTransaction::new_v2();
375
376        for i in 0..self.inputs.len() {
377            let final_input = &self.inputs[i];
378            let pst_input = final_input.to_input();
379
380            match final_input.partial_input.secrets {
381                // insert input secrets if present
382                Some(secrets) => input_secrets.insert(i, secrets),
383                // else populate input secrets with "explicit" amounts
384                None => input_secrets.insert(
385                    i,
386                    TxOutSecrets {
387                        asset: pst_input.asset.unwrap(),
388                        asset_bf: AssetBlindingFactor::zero(),
389                        value: pst_input.amount.unwrap(),
390                        value_bf: ValueBlindingFactor::zero(),
391                    },
392                ),
393            };
394
395            pst.add_input(pst_input);
396        }
397
398        self.outputs.iter().for_each(|el| {
399            pst.add_output(el.to_output());
400        });
401
402        (pst, input_secrets)
403    }
404
405    fn push_new_input(&mut self, new_input: FinalInput) -> Option<IssuanceDetails> {
406        let issuance_details = new_input.get_issuance_details();
407
408        self.inputs.push(new_input);
409
410        issuance_details
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use bitcoin_hashes::Hash;
417
418    use simplicityhl::elements::{OutPoint, Script, TxOut, Txid};
419
420    use crate::transaction::UTXO;
421
422    use super::*;
423
424    fn dummy_asset_id(byte: u8) -> AssetId {
425        AssetId::from_slice(&[byte; 32]).unwrap()
426    }
427
428    fn dummy_txid(byte: u8) -> Txid {
429        Txid::from_slice(&[byte; 32]).unwrap()
430    }
431
432    fn explicit_utxo(txid_byte: u8, vout: u32, amount: u64, asset: AssetId) -> UTXO {
433        UTXO {
434            outpoint: OutPoint::new(dummy_txid(txid_byte), vout),
435            txout: TxOut::new_fee(amount, asset),
436            secrets: None,
437        }
438    }
439
440    fn confidential_utxo(txid_byte: u8, vout: u32, asset: AssetId, value: u64) -> UTXO {
441        UTXO {
442            outpoint: OutPoint::new(dummy_txid(txid_byte), vout),
443            txout: TxOut::default(),
444            secrets: Some(TxOutSecrets::new(
445                asset,
446                AssetBlindingFactor::zero(),
447                value,
448                ValueBlindingFactor::zero(),
449            )),
450        }
451    }
452
453    // Manually construct PST and check extract_pst correctness based on it
454    #[test]
455    fn extract_pst_single_explicit_input_single_output() {
456        let policy = dummy_asset_id(0xAA);
457
458        let utxo = explicit_utxo(0x01, 0, 5000, policy);
459        let partial_input = PartialInput::new(utxo);
460        let partial_output = PartialOutput::new(Script::new(), 4000, policy);
461
462        let mut ft = FinalTransaction::new();
463        ft.add_input(partial_input.clone(), RequiredSignature::None);
464        ft.add_output(partial_output.clone());
465
466        let mut expected_pst = PartiallySignedTransaction::new_v2();
467        expected_pst.add_input(partial_input.to_input());
468        expected_pst.add_output(partial_output.to_output());
469
470        let expected_secrets: HashMap<usize, TxOutSecrets> = HashMap::from([(
471            0,
472            TxOutSecrets::new(policy, AssetBlindingFactor::zero(), 5000, ValueBlindingFactor::zero()),
473        )]);
474
475        let (pst, secrets) = ft.extract_pst();
476
477        assert_eq!(pst, expected_pst);
478        assert_eq!(secrets, expected_secrets);
479    }
480
481    #[test]
482    fn extract_pst_single_confidential_input() {
483        let policy = dummy_asset_id(0xAA);
484
485        let utxo = confidential_utxo(0x01, 0, policy, 3000);
486        let partial_input = PartialInput::new(utxo);
487        let partial_output = PartialOutput::new(Script::new(), 2000, policy);
488
489        let mut ft = FinalTransaction::new();
490        ft.add_input(partial_input.clone(), RequiredSignature::None);
491        ft.add_output(partial_output.clone());
492
493        let mut expected_pst = PartiallySignedTransaction::new_v2();
494        expected_pst.add_input(partial_input.to_input());
495        expected_pst.add_output(partial_output.to_output());
496
497        let expected_secrets = HashMap::from([(
498            0,
499            TxOutSecrets::new(policy, AssetBlindingFactor::zero(), 3000, ValueBlindingFactor::zero()),
500        )]);
501
502        let (pst, secrets) = ft.extract_pst();
503
504        assert_eq!(pst, expected_pst);
505        assert_eq!(secrets, expected_secrets);
506    }
507
508    #[test]
509    fn extract_pst_mixed_inputs_multiple_outputs() {
510        let policy = dummy_asset_id(0xAA);
511        let other = dummy_asset_id(0xBB);
512
513        let explicit_utxo = explicit_utxo(0x01, 0, 5000, policy);
514        let conf_utxo = confidential_utxo(0x02, 1, other, 1000);
515
516        let explicit_partial = PartialInput::new(explicit_utxo);
517        let conf_partial = PartialInput::new(conf_utxo);
518
519        let output_a = PartialOutput::new(Script::new(), 3000, policy);
520        let output_b = PartialOutput::new(Script::new(), 800, other);
521
522        let mut ft = FinalTransaction::new();
523        ft.add_input(explicit_partial.clone(), RequiredSignature::None);
524        ft.add_input(conf_partial.clone(), RequiredSignature::None);
525        ft.add_output(output_a.clone());
526        ft.add_output(output_b.clone());
527
528        let mut expected_pst = PartiallySignedTransaction::new_v2();
529        expected_pst.add_input(explicit_partial.to_input());
530        expected_pst.add_input(conf_partial.to_input());
531        expected_pst.add_output(output_a.to_output());
532        expected_pst.add_output(output_b.to_output());
533
534        let expected_secrets = HashMap::from([
535            (
536                0,
537                TxOutSecrets::new(policy, AssetBlindingFactor::zero(), 5000, ValueBlindingFactor::zero()),
538            ),
539            (
540                1,
541                TxOutSecrets::new(other, AssetBlindingFactor::zero(), 1000, ValueBlindingFactor::zero()),
542            ),
543        ]);
544
545        let (pst, secrets) = ft.extract_pst();
546
547        assert_eq!(pst, expected_pst);
548        assert_eq!(secrets, expected_secrets);
549    }
550
551    #[test]
552    fn extract_pst_with_issuance_input() {
553        let policy = dummy_asset_id(0xAA);
554        let entropy = [0x42u8; 32];
555        let issuance_amount = 1_000_000u64;
556
557        let utxo = explicit_utxo(0x01, 0, 5000, policy);
558        let partial_input = PartialInput::new(utxo);
559        let issuance = IssuanceInput::new_issuance(issuance_amount, 0, entropy);
560        let partial_output = PartialOutput::new(Script::new(), 4000, policy);
561
562        let mut ft = FinalTransaction::new();
563        ft.add_issuance_input(partial_input.clone(), issuance.clone(), RequiredSignature::None);
564        ft.add_output(partial_output.clone());
565
566        // build expected pst, merge partial_input and issuance manually
567        let mut expected_pst = PartiallySignedTransaction::new_v2();
568        let mut expected_input = partial_input.to_input();
569        let issuance_input = issuance.to_input();
570        expected_input.issuance_value_amount = issuance_input.issuance_value_amount;
571        expected_input.issuance_asset_entropy = issuance_input.issuance_asset_entropy;
572        expected_input.issuance_inflation_keys = issuance_input.issuance_inflation_keys;
573        expected_input.issuance_blinding_nonce = None;
574        expected_input.blinded_issuance = issuance_input.blinded_issuance;
575        expected_pst.add_input(expected_input);
576        expected_pst.add_output(partial_output.to_output());
577
578        let expected_secrets = HashMap::from([(
579            0,
580            TxOutSecrets::new(policy, AssetBlindingFactor::zero(), 5000, ValueBlindingFactor::zero()),
581        )]);
582
583        let (pst, secrets) = ft.extract_pst();
584
585        assert_eq!(pst, expected_pst);
586        assert_eq!(secrets, expected_secrets);
587    }
588
589    #[test]
590    fn extract_pst_with_reissuance_input() {
591        let policy = dummy_asset_id(0xAA);
592        let entropy = [0x42u8; 32];
593        let issuance_amount = 1_000_000u64;
594
595        let conf_utxo = confidential_utxo(0x02, 0, policy, 1000);
596        let partial_input = PartialInput::new(conf_utxo);
597        let reissuance_input = IssuanceInput::new_reissuance(issuance_amount, entropy);
598        let partial_output = PartialOutput::new(Script::new(), 1000, policy);
599
600        let mut ft = FinalTransaction::new();
601        ft.add_issuance_input(partial_input.clone(), reissuance_input.clone(), RequiredSignature::None);
602        ft.add_output(partial_output.clone());
603
604        // build expected pst, merge partial_input and issuance manually
605        let mut expected_pst = PartiallySignedTransaction::new_v2();
606        let mut expected_input = partial_input.to_input();
607        let issuance_input = reissuance_input.to_input();
608        expected_input.issuance_value_amount = issuance_input.issuance_value_amount;
609        expected_input.issuance_asset_entropy = issuance_input.issuance_asset_entropy;
610        expected_input.issuance_inflation_keys = None;
611        expected_input.issuance_blinding_nonce = Some(partial_input.secrets.unwrap().asset_bf.into_inner());
612        expected_input.blinded_issuance = issuance_input.blinded_issuance;
613        expected_pst.add_input(expected_input);
614        expected_pst.add_output(partial_output.to_output());
615
616        let expected_secrets = HashMap::from([(
617            0,
618            TxOutSecrets::new(policy, AssetBlindingFactor::zero(), 1000, ValueBlindingFactor::zero()),
619        )]);
620
621        let (pst, secrets) = ft.extract_pst();
622
623        assert_eq!(pst, expected_pst);
624        assert_eq!(secrets, expected_secrets);
625    }
626}