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        match required_sig {
42            RequiredSignature::Witness(_) | RequiredSignature::WitnessWithPath(_, _) => {
43                panic!("Requested signature is not NativeEcdsa or None")
44            }
45            _ => {}
46        };
47
48        self.inputs.push(FinalInput {
49            partial_input,
50            program_input: None,
51            issuance_input: None,
52            required_sig,
53        });
54    }
55
56    pub fn add_program_input(
57        &mut self,
58        partial_input: PartialInput,
59        program_input: ProgramInput,
60        required_sig: RequiredSignature,
61    ) {
62        if let RequiredSignature::NativeEcdsa = required_sig {
63            panic!("Requested signature is not Witness or None");
64        }
65
66        self.inputs.push(FinalInput {
67            partial_input,
68            program_input: Some(program_input),
69            issuance_input: None,
70            required_sig,
71        });
72    }
73
74    pub fn add_issuance_input(
75        &mut self,
76        partial_input: PartialInput,
77        issuance_input: IssuanceInput,
78        required_sig: RequiredSignature,
79    ) -> (AssetId, AssetId) {
80        match required_sig {
81            RequiredSignature::Witness(_) | RequiredSignature::WitnessWithPath(_, _) => {
82                panic!("Requested signature is not NativeEcdsa or None")
83            }
84            _ => {}
85        };
86
87        let entropy = asset_entropy(&partial_input.outpoint(), issuance_input.asset_entropy);
88
89        let issuance_asset_id = AssetId::from_entropy(entropy);
90        let reissuance_asset_id = AssetId::reissuance_token_from_entropy(entropy, false);
91
92        self.inputs.push(FinalInput {
93            partial_input,
94            program_input: None,
95            issuance_input: Some(issuance_input),
96            required_sig,
97        });
98
99        (issuance_asset_id, reissuance_asset_id)
100    }
101
102    pub fn add_program_issuance_input(
103        &mut self,
104        partial_input: PartialInput,
105        program_input: ProgramInput,
106        issuance_input: IssuanceInput,
107        required_sig: RequiredSignature,
108    ) -> (AssetId, AssetId) {
109        if let RequiredSignature::NativeEcdsa = required_sig {
110            panic!("Requested signature is not Witness or None");
111        }
112
113        let entropy = asset_entropy(&partial_input.outpoint(), issuance_input.asset_entropy);
114
115        let issuance_asset_id = AssetId::from_entropy(entropy);
116        let reissuance_asset_id = AssetId::reissuance_token_from_entropy(entropy, false);
117
118        self.inputs.push(FinalInput {
119            partial_input,
120            program_input: Some(program_input),
121            issuance_input: Some(issuance_input),
122            required_sig,
123        });
124
125        (issuance_asset_id, reissuance_asset_id)
126    }
127
128    pub fn remove_input(&mut self, index: usize) -> Option<FinalInput> {
129        if self.inputs.get(index).is_some() {
130            return Some(self.inputs.remove(index));
131        }
132
133        None
134    }
135
136    pub fn add_output(&mut self, partial_output: PartialOutput) {
137        self.outputs.push(partial_output);
138    }
139
140    pub fn remove_output(&mut self, index: usize) -> Option<PartialOutput> {
141        if self.outputs.get(index).is_some() {
142            return Some(self.outputs.remove(index));
143        }
144
145        None
146    }
147
148    pub fn inputs(&self) -> &[FinalInput] {
149        &self.inputs
150    }
151
152    pub fn inputs_mut(&mut self) -> &mut [FinalInput] {
153        &mut self.inputs
154    }
155
156    pub fn outputs(&self) -> &[PartialOutput] {
157        &self.outputs
158    }
159
160    pub fn outputs_mut(&mut self) -> &mut [PartialOutput] {
161        &mut self.outputs
162    }
163
164    pub fn n_inputs(&self) -> usize {
165        self.inputs.len()
166    }
167
168    pub fn n_outputs(&self) -> usize {
169        self.outputs.len()
170    }
171
172    pub fn needs_blinding(&self) -> bool {
173        self.outputs.iter().any(|el| el.blinding_key.is_some())
174    }
175
176    pub fn calculate_fee_delta(&self, network: &SimplicityNetwork) -> i64 {
177        let mut available_amount = 0;
178
179        for input in &self.inputs {
180            match input.partial_input.secrets {
181                // this is an unblinded confidential input
182                Some(secrets) => {
183                    if secrets.asset == network.policy_asset() {
184                        available_amount += secrets.value;
185                    }
186                }
187                // this is an explicit input
188                None => {
189                    if input.partial_input.asset.unwrap() == network.policy_asset() {
190                        available_amount += input.partial_input.amount.unwrap();
191                    }
192                }
193            }
194        }
195
196        let consumed_amount = self
197            .outputs
198            .iter()
199            .filter(|output| output.asset == network.policy_asset())
200            .fold(0_u64, |acc, output| acc + output.amount);
201
202        available_amount as i64 - consumed_amount as i64
203    }
204
205    pub fn calculate_fee(&self, weight: usize, fee_rate: f32) -> u64 {
206        let vsize = weight.div_ceil(WITNESS_SCALE_FACTOR);
207
208        (vsize as f32 * fee_rate / 1000.0).ceil() as u64
209    }
210
211    pub fn extract_pst(&self) -> (PartiallySignedTransaction, HashMap<usize, TxOutSecrets>) {
212        let mut input_secrets = HashMap::new();
213        let mut pst = PartiallySignedTransaction::new_v2();
214
215        for i in 0..self.inputs.len() {
216            let final_input = &self.inputs[i];
217            let mut pst_input = final_input.partial_input.to_input();
218
219            // populate the input manually since `input.merge` is private
220            if final_input.issuance_input.is_some() {
221                let issue = final_input.issuance_input.clone().unwrap().to_input();
222
223                pst_input.issuance_value_amount = issue.issuance_value_amount;
224                pst_input.issuance_asset_entropy = issue.issuance_asset_entropy;
225                pst_input.issuance_inflation_keys = issue.issuance_inflation_keys;
226                pst_input.blinded_issuance = issue.blinded_issuance;
227            }
228
229            match final_input.partial_input.secrets {
230                // insert input secrets if present
231                Some(secrets) => input_secrets.insert(i, secrets),
232                // else populate input secrets with "explicit" amounts
233                None => input_secrets.insert(
234                    i,
235                    TxOutSecrets {
236                        asset: pst_input.asset.unwrap(),
237                        asset_bf: AssetBlindingFactor::zero(),
238                        value: pst_input.amount.unwrap(),
239                        value_bf: ValueBlindingFactor::zero(),
240                    },
241                ),
242            };
243
244            pst.add_input(pst_input);
245        }
246
247        self.outputs.iter().for_each(|el| {
248            pst.add_output(el.to_output());
249        });
250
251        (pst, input_secrets)
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use bitcoin_hashes::Hash;
258
259    use simplicityhl::elements::{OutPoint, Script, TxOut, Txid};
260
261    use crate::transaction::UTXO;
262
263    use super::*;
264
265    fn dummy_asset_id(byte: u8) -> AssetId {
266        AssetId::from_slice(&[byte; 32]).unwrap()
267    }
268
269    fn dummy_txid(byte: u8) -> Txid {
270        Txid::from_slice(&[byte; 32]).unwrap()
271    }
272
273    fn explicit_utxo(txid_byte: u8, vout: u32, amount: u64, asset: AssetId) -> UTXO {
274        UTXO {
275            outpoint: OutPoint::new(dummy_txid(txid_byte), vout),
276            txout: TxOut::new_fee(amount, asset),
277            secrets: None,
278        }
279    }
280
281    fn confidential_utxo(txid_byte: u8, vout: u32, asset: AssetId, value: u64) -> UTXO {
282        UTXO {
283            outpoint: OutPoint::new(dummy_txid(txid_byte), vout),
284            txout: TxOut::default(),
285            secrets: Some(TxOutSecrets::new(
286                asset,
287                AssetBlindingFactor::zero(),
288                value,
289                ValueBlindingFactor::zero(),
290            )),
291        }
292    }
293
294    // Manually construct PST and check extract_pst correctness based on it
295    #[test]
296    fn extract_pst_single_explicit_input_single_output() {
297        let policy = dummy_asset_id(0xAA);
298
299        let utxo = explicit_utxo(0x01, 0, 5000, policy);
300        let partial_input = PartialInput::new(utxo);
301        let partial_output = PartialOutput::new(Script::new(), 4000, policy);
302
303        let mut ft = FinalTransaction::new();
304        ft.add_input(partial_input.clone(), RequiredSignature::None);
305        ft.add_output(partial_output.clone());
306
307        let mut expected_pst = PartiallySignedTransaction::new_v2();
308        expected_pst.add_input(partial_input.to_input());
309        expected_pst.add_output(partial_output.to_output());
310
311        let expected_secrets: HashMap<usize, TxOutSecrets> = HashMap::from([(
312            0,
313            TxOutSecrets::new(policy, AssetBlindingFactor::zero(), 5000, ValueBlindingFactor::zero()),
314        )]);
315
316        let (pst, secrets) = ft.extract_pst();
317
318        assert_eq!(pst, expected_pst);
319        assert_eq!(secrets, expected_secrets);
320    }
321
322    #[test]
323    fn extract_pst_single_confidential_input() {
324        let policy = dummy_asset_id(0xAA);
325
326        let utxo = confidential_utxo(0x01, 0, policy, 3000);
327        let partial_input = PartialInput::new(utxo);
328        let partial_output = PartialOutput::new(Script::new(), 2000, policy);
329
330        let mut ft = FinalTransaction::new();
331        ft.add_input(partial_input.clone(), RequiredSignature::None);
332        ft.add_output(partial_output.clone());
333
334        let mut expected_pst = PartiallySignedTransaction::new_v2();
335        expected_pst.add_input(partial_input.to_input());
336        expected_pst.add_output(partial_output.to_output());
337
338        let expected_secrets = HashMap::from([(
339            0,
340            TxOutSecrets::new(policy, AssetBlindingFactor::zero(), 3000, ValueBlindingFactor::zero()),
341        )]);
342
343        let (pst, secrets) = ft.extract_pst();
344
345        assert_eq!(pst, expected_pst);
346        assert_eq!(secrets, expected_secrets);
347    }
348
349    #[test]
350    fn extract_pst_mixed_inputs_multiple_outputs() {
351        let policy = dummy_asset_id(0xAA);
352        let other = dummy_asset_id(0xBB);
353
354        let explicit_utxo = explicit_utxo(0x01, 0, 5000, policy);
355        let conf_utxo = confidential_utxo(0x02, 1, other, 1000);
356
357        let explicit_partial = PartialInput::new(explicit_utxo);
358        let conf_partial = PartialInput::new(conf_utxo);
359
360        let output_a = PartialOutput::new(Script::new(), 3000, policy);
361        let output_b = PartialOutput::new(Script::new(), 800, other);
362
363        let mut ft = FinalTransaction::new();
364        ft.add_input(explicit_partial.clone(), RequiredSignature::None);
365        ft.add_input(conf_partial.clone(), RequiredSignature::None);
366        ft.add_output(output_a.clone());
367        ft.add_output(output_b.clone());
368
369        let mut expected_pst = PartiallySignedTransaction::new_v2();
370        expected_pst.add_input(explicit_partial.to_input());
371        expected_pst.add_input(conf_partial.to_input());
372        expected_pst.add_output(output_a.to_output());
373        expected_pst.add_output(output_b.to_output());
374
375        let expected_secrets = HashMap::from([
376            (
377                0,
378                TxOutSecrets::new(policy, AssetBlindingFactor::zero(), 5000, ValueBlindingFactor::zero()),
379            ),
380            (
381                1,
382                TxOutSecrets::new(other, AssetBlindingFactor::zero(), 1000, ValueBlindingFactor::zero()),
383            ),
384        ]);
385
386        let (pst, secrets) = ft.extract_pst();
387
388        assert_eq!(pst, expected_pst);
389        assert_eq!(secrets, expected_secrets);
390    }
391
392    #[test]
393    fn extract_pst_with_issuance_input() {
394        let policy = dummy_asset_id(0xAA);
395        let entropy = [0x42u8; 32];
396        let issuance_amount = 1_000_000u64;
397
398        let utxo = explicit_utxo(0x01, 0, 5000, policy);
399        let partial_input = PartialInput::new(utxo);
400        let issuance = IssuanceInput::new(issuance_amount, entropy);
401        let partial_output = PartialOutput::new(Script::new(), 4000, policy);
402
403        let mut ft = FinalTransaction::new();
404        ft.add_issuance_input(partial_input.clone(), issuance.clone(), RequiredSignature::None);
405        ft.add_output(partial_output.clone());
406
407        // build expected pst, merge partial_input and issuance manually
408        let mut expected_pst = PartiallySignedTransaction::new_v2();
409        let mut expected_input = partial_input.to_input();
410        let issuance_input = issuance.to_input();
411        expected_input.issuance_value_amount = issuance_input.issuance_value_amount;
412        expected_input.issuance_asset_entropy = issuance_input.issuance_asset_entropy;
413        expected_input.issuance_inflation_keys = issuance_input.issuance_inflation_keys;
414        expected_input.blinded_issuance = issuance_input.blinded_issuance;
415        expected_pst.add_input(expected_input);
416        expected_pst.add_output(partial_output.to_output());
417
418        let expected_secrets = HashMap::from([(
419            0,
420            TxOutSecrets::new(policy, AssetBlindingFactor::zero(), 5000, ValueBlindingFactor::zero()),
421        )]);
422
423        let (pst, secrets) = ft.extract_pst();
424
425        assert_eq!(pst, expected_pst);
426        assert_eq!(secrets, expected_secrets);
427    }
428}