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 Some(secrets) => {
183 if secrets.asset == network.policy_asset() {
184 available_amount += secrets.value;
185 }
186 }
187 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 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 Some(secrets) => input_secrets.insert(i, secrets),
232 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 #[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 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}