Skip to main content

smplx_sdk/transaction/
final_transaction.rs

1use std::collections::HashMap;
2
3use simplicityhl::elements::pset::PartiallySignedTransaction;
4use simplicityhl::elements::{
5    AssetId, TxOutSecrets,
6    confidential::{AssetBlindingFactor, ValueBlindingFactor},
7};
8
9use crate::provider::SimplicityNetwork;
10use crate::utils::asset_entropy;
11
12use super::partial_input::{IssuanceInput, PartialInput, ProgramInput, RequiredSignature};
13use super::partial_output::PartialOutput;
14
15pub const WITNESS_SCALE_FACTOR: usize = 4;
16
17#[derive(Clone)]
18pub struct FinalInput {
19    pub partial_input: PartialInput,
20    pub program_input: Option<ProgramInput>,
21    pub issuance_input: Option<IssuanceInput>,
22    pub required_sig: RequiredSignature,
23}
24
25#[derive(Clone)]
26pub struct FinalTransaction {
27    inputs: Vec<FinalInput>,
28    outputs: Vec<PartialOutput>,
29}
30
31impl FinalTransaction {
32    #[allow(clippy::new_without_default)]
33    pub fn new() -> Self {
34        Self {
35            inputs: Vec::new(),
36            outputs: Vec::new(),
37        }
38    }
39
40    pub fn add_input(&mut self, partial_input: PartialInput, required_sig: RequiredSignature) {
41        if let RequiredSignature::Witness(_) = required_sig {
42            panic!("Requested signature is not NativeEcdsa or None");
43        }
44
45        self.inputs.push(FinalInput {
46            partial_input,
47            program_input: None,
48            issuance_input: None,
49            required_sig,
50        });
51    }
52
53    pub fn add_program_input(
54        &mut self,
55        partial_input: PartialInput,
56        program_input: ProgramInput,
57        required_sig: RequiredSignature,
58    ) {
59        if let RequiredSignature::NativeEcdsa = required_sig {
60            panic!("Requested signature is not Witness or None");
61        }
62
63        self.inputs.push(FinalInput {
64            partial_input,
65            program_input: Some(program_input),
66            issuance_input: None,
67            required_sig,
68        });
69    }
70
71    pub fn add_issuance_input(
72        &mut self,
73        partial_input: PartialInput,
74        issuance_input: IssuanceInput,
75        required_sig: RequiredSignature,
76    ) -> AssetId {
77        if let RequiredSignature::Witness(_) = required_sig {
78            panic!("Requested signature is not NativeEcdsa or None");
79        }
80
81        let asset_id = AssetId::from_entropy(asset_entropy(&partial_input.outpoint(), issuance_input.asset_entropy));
82
83        self.inputs.push(FinalInput {
84            partial_input,
85            program_input: None,
86            issuance_input: Some(issuance_input),
87            required_sig,
88        });
89
90        asset_id
91    }
92
93    pub fn add_program_issuance_input(
94        &mut self,
95        partial_input: PartialInput,
96        program_input: ProgramInput,
97        issuance_input: IssuanceInput,
98        required_sig: RequiredSignature,
99    ) -> AssetId {
100        if let RequiredSignature::NativeEcdsa = required_sig {
101            panic!("Requested signature is not Witness or None");
102        }
103
104        let asset_id = AssetId::from_entropy(asset_entropy(&partial_input.outpoint(), issuance_input.asset_entropy));
105
106        self.inputs.push(FinalInput {
107            partial_input,
108            program_input: Some(program_input),
109            issuance_input: Some(issuance_input),
110            required_sig,
111        });
112
113        asset_id
114    }
115
116    pub fn remove_input(&mut self, index: usize) -> Option<FinalInput> {
117        if self.inputs.get(index).is_some() {
118            return Some(self.inputs.remove(index));
119        }
120
121        None
122    }
123
124    pub fn add_output(&mut self, partial_output: PartialOutput) {
125        self.outputs.push(partial_output);
126    }
127
128    pub fn remove_output(&mut self, index: usize) -> Option<PartialOutput> {
129        if self.outputs.get(index).is_some() {
130            return Some(self.outputs.remove(index));
131        }
132
133        None
134    }
135
136    pub fn inputs(&self) -> &[FinalInput] {
137        &self.inputs
138    }
139
140    pub fn inputs_mut(&mut self) -> &mut [FinalInput] {
141        &mut self.inputs
142    }
143
144    pub fn outputs(&self) -> &[PartialOutput] {
145        &self.outputs
146    }
147
148    pub fn outputs_mut(&mut self) -> &mut [PartialOutput] {
149        &mut self.outputs
150    }
151
152    pub fn n_inputs(&self) -> usize {
153        self.inputs.len()
154    }
155
156    pub fn n_outputs(&self) -> usize {
157        self.outputs.len()
158    }
159
160    pub fn needs_blinding(&self) -> bool {
161        self.outputs.iter().any(|el| el.blinding_key.is_some())
162    }
163
164    pub fn calculate_fee_delta(&self, network: &SimplicityNetwork) -> i64 {
165        let mut available_amount = 0;
166
167        for input in &self.inputs {
168            match input.partial_input.secrets {
169                // this is an unblinded confidential input
170                Some(secrets) => {
171                    if secrets.asset == network.policy_asset() {
172                        available_amount += secrets.value;
173                    }
174                }
175                // this is an explicit input
176                None => {
177                    if input.partial_input.asset.unwrap() == network.policy_asset() {
178                        available_amount += input.partial_input.amount.unwrap();
179                    }
180                }
181            }
182        }
183
184        let consumed_amount = self
185            .outputs
186            .iter()
187            .filter(|output| output.asset == network.policy_asset())
188            .fold(0_u64, |acc, output| acc + output.amount);
189
190        available_amount as i64 - consumed_amount as i64
191    }
192
193    pub fn calculate_fee(&self, weight: usize, fee_rate: f32) -> u64 {
194        let vsize = weight.div_ceil(WITNESS_SCALE_FACTOR);
195
196        (vsize as f32 * fee_rate / 1000.0).ceil() as u64
197    }
198
199    pub fn extract_pst(&self) -> (PartiallySignedTransaction, HashMap<usize, TxOutSecrets>) {
200        let mut input_secrets = HashMap::new();
201        let mut pst = PartiallySignedTransaction::new_v2();
202
203        for i in 0..self.inputs.len() {
204            let final_input = &self.inputs[i];
205            let mut pst_input = final_input.partial_input.to_input();
206
207            // populate the input manually since `input.merge` is private
208            if final_input.issuance_input.is_some() {
209                let issue = final_input.issuance_input.clone().unwrap().to_input();
210
211                pst_input.issuance_value_amount = issue.issuance_value_amount;
212                pst_input.issuance_asset_entropy = issue.issuance_asset_entropy;
213                pst_input.issuance_inflation_keys = issue.issuance_inflation_keys;
214                pst_input.blinded_issuance = issue.blinded_issuance;
215            }
216
217            match final_input.partial_input.secrets {
218                // insert input secrets if present
219                Some(secrets) => input_secrets.insert(i, secrets),
220                // else populate input secrets with "explicit" amounts
221                None => input_secrets.insert(
222                    i,
223                    TxOutSecrets {
224                        asset: pst_input.asset.unwrap(),
225                        asset_bf: AssetBlindingFactor::zero(),
226                        value: pst_input.amount.unwrap(),
227                        value_bf: ValueBlindingFactor::zero(),
228                    },
229                ),
230            };
231
232            pst.add_input(pst_input);
233        }
234
235        self.outputs.iter().for_each(|el| {
236            pst.add_output(el.to_output());
237        });
238
239        (pst, input_secrets)
240    }
241}