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
17pub const WITNESS_SCALE_FACTOR: usize = 4;
19
20#[derive(Debug, Clone)]
22pub struct IssuanceDetails {
23 pub asset_id: AssetId,
25 pub inflation_asset_id: AssetId,
27 pub asset_entropy: sha256::Midstate,
29}
30
31#[derive(Clone)]
33pub struct FinalInput {
34 pub partial_input: PartialInput,
36 pub program_input: Option<ProgramInput>,
38 pub issuance_input: Option<IssuanceInput>,
40 pub required_sig: RequiredSignature,
42}
43
44impl FinalInput {
45 #[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 #[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 #[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 #[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 #[must_use]
113 pub fn to_input(&self) -> Input {
114 let mut pst_input = self.partial_input.to_input();
115
116 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#[derive(Clone)]
143pub struct FinalTransaction {
144 inputs: Vec<FinalInput>,
145 outputs: Vec<PartialOutput>,
146}
147
148impl FinalTransaction {
149 #[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 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 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 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 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 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 pub fn add_output(&mut self, partial_output: PartialOutput) {
250 self.outputs.push(partial_output);
251 }
252
253 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 #[must_use]
267 pub fn inputs(&self) -> &[FinalInput] {
268 &self.inputs
269 }
270
271 pub fn inputs_mut(&mut self) -> &mut [FinalInput] {
276 &mut self.inputs
277 }
278
279 #[must_use]
281 pub fn outputs(&self) -> &[PartialOutput] {
282 &self.outputs
283 }
284
285 pub fn outputs_mut(&mut self) -> &mut [PartialOutput] {
287 &mut self.outputs
288 }
289
290 #[must_use]
292 pub fn n_inputs(&self) -> usize {
293 self.inputs.len()
294 }
295
296 #[must_use]
298 pub fn n_outputs(&self) -> usize {
299 self.outputs.len()
300 }
301
302 #[must_use]
304 pub fn needs_blinding(&self) -> bool {
305 self.outputs.iter().any(|el| el.blinding_key.is_some())
306 }
307
308 #[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 Some(secrets) => {
325 if secrets.asset == network.policy_asset() {
326 available_amount += secrets.value;
327 }
328 }
329 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 #[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 #[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 Some(secrets) => input_secrets.insert(i, secrets),
383 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 #[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 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 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}