Skip to main content

pallas_txbuilder/transaction/
model.rs

1use pallas_addresses::Address as PallasAddress;
2use pallas_crypto::{
3    hash::{Hash, Hasher},
4    key::ed25519,
5};
6use pallas_primitives::{
7    Fragment, NonEmptySet,
8    conway::{self, AuxiliaryData},
9};
10
11use std::{collections::HashMap, ops::Deref};
12
13use serde::{Deserialize, Serialize};
14
15use crate::TxBuilderError;
16
17use super::{
18    AssetName, Bytes, Bytes32, Bytes64, DatumBytes, DatumHash, Hash28, PolicyId, PubKeyHash,
19    PublicKey, ScriptBytes, ScriptHash, Signature, TransactionStatus, TxHash,
20};
21use pallas_codec::minicbor;
22// TODO: Don't make wrapper types public
23/// In-progress transaction that converts to a [`BuiltTransaction`] via an era-specific build trait.
24#[derive(Default, Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
25pub struct StagingTransaction {
26    /// Schema version of this staging document (for serialization compat).
27    pub version: String,
28    /// Lifecycle state of this transaction.
29    pub status: TransactionStatus,
30    /// Inputs consumed by this transaction.
31    pub inputs: Option<Vec<Input>>,
32    /// Reference inputs read but not consumed (CIP-31).
33    pub reference_inputs: Option<Vec<Input>>,
34    /// Outputs produced by this transaction.
35    pub outputs: Option<Vec<Output>>,
36    /// Fee in lovelace (must already include script execution costs).
37    pub fee: Option<u64>,
38    /// Tokens minted or burned by this transaction.
39    pub mint: Option<MintAssets>,
40    /// Earliest slot at which this transaction is valid.
41    pub valid_from_slot: Option<u64>,
42    /// Slot at which this transaction becomes invalid (TTL).
43    pub invalid_from_slot: Option<u64>,
44    /// Network id this transaction targets (0 testnet, 1 mainnet).
45    pub network_id: Option<u8>,
46    /// Inputs offered as collateral for phase-2 script evaluation.
47    pub collateral_inputs: Option<Vec<Input>>,
48    /// Output receiving leftover collateral when scripts succeed.
49    pub collateral_output: Option<Output>,
50    /// Required-signer key hashes hinted to script evaluation.
51    pub disclosed_signers: Option<Vec<PubKeyHash>>,
52    /// Native and Plutus scripts attached to the transaction, keyed by hash.
53    pub scripts: Option<HashMap<ScriptHash, Script>>,
54    /// Plutus datums attached to the transaction, keyed by datum hash.
55    pub datums: Option<HashMap<DatumHash, DatumBytes>>,
56    /// Plutus redeemers paired with their target script purpose.
57    pub redeemers: Option<Redeemers>,
58    /// Cached script-data hash; recomputed at build time when scripts are present.
59    pub script_data_hash: Option<Bytes32>,
60    /// Override for the assumed number of signatures (fee estimation).
61    pub signature_amount_override: Option<u8>,
62    /// Address receiving change when the builder balances the transaction.
63    pub change_address: Option<Address>,
64    /// Plutus language-view CBOR encodings used in the script-data hash.
65    pub language_views: Option<pallas_primitives::conway::LanguageViews>,
66    /// Auxiliary data (metadata, native scripts) attached to the transaction.
67    pub auxiliary_data: Option<AuxiliaryData>,
68    // pub certificates: TODO
69    // pub withdrawals: TODO
70    // pub updates: TODO
71    // pub phase_2_valid: TODO
72}
73
74impl StagingTransaction {
75    /// Create an empty staging transaction in the `Staging` status.
76    pub fn new() -> Self {
77        Self {
78            version: String::from("v1"),
79            status: TransactionStatus::Staging,
80            ..Default::default()
81        }
82    }
83
84    /// Append a consumed input.
85    pub fn input(mut self, input: Input) -> Self {
86        let mut txins = self.inputs.unwrap_or_default();
87        txins.push(input);
88        self.inputs = Some(txins);
89        self
90    }
91
92    /// Remove a previously added consumed input.
93    pub fn remove_input(mut self, input: Input) -> Self {
94        let mut txins = self.inputs.unwrap_or_default();
95        txins.retain(|x| *x != input);
96        self.inputs = Some(txins);
97        self
98    }
99
100    /// Append a reference input (CIP-31; read-only, not spent).
101    pub fn reference_input(mut self, input: Input) -> Self {
102        let mut ref_txins = self.reference_inputs.unwrap_or_default();
103        ref_txins.push(input);
104        self.reference_inputs = Some(ref_txins);
105        self
106    }
107
108    /// Remove a previously added reference input.
109    pub fn remove_reference_input(mut self, input: Input) -> Self {
110        let mut ref_txins = self.reference_inputs.unwrap_or_default();
111        ref_txins.retain(|x| *x != input);
112        self.reference_inputs = Some(ref_txins);
113        self
114    }
115
116    /// Append a produced output.
117    pub fn output(mut self, output: Output) -> Self {
118        let mut txouts = self.outputs.unwrap_or_default();
119        txouts.push(output);
120        self.outputs = Some(txouts);
121        self
122    }
123
124    /// Remove the output at the given index.
125    pub fn remove_output(mut self, index: usize) -> Self {
126        let mut txouts = self.outputs.unwrap_or_default();
127        txouts.remove(index);
128        self.outputs = Some(txouts);
129        self
130    }
131
132    /// Set the fee (lovelace).
133    pub fn fee(mut self, fee: u64) -> Self {
134        self.fee = Some(fee);
135        self
136    }
137
138    /// Clear any previously set fee.
139    pub fn clear_fee(mut self) -> Self {
140        self.fee = None;
141        self
142    }
143
144    /// Add (or accumulate) a mint/burn quantity for `(policy, name)`. Positive
145    /// values mint; negative values burn. Fails if the asset name exceeds 32 bytes.
146    pub fn mint_asset(
147        mut self,
148        policy: Hash<28>,
149        name: Vec<u8>,
150        amount: i64,
151    ) -> Result<Self, TxBuilderError> {
152        if name.len() > 32 {
153            return Err(TxBuilderError::AssetNameTooLong);
154        }
155
156        let mut mint = self.mint.map(|x| x.0).unwrap_or_default();
157
158        mint.entry(Hash28(*policy))
159            .and_modify(|policy_map| {
160                policy_map
161                    .entry(name.clone().into())
162                    .and_modify(|asset_map| {
163                        *asset_map += amount;
164                    })
165                    .or_insert(amount);
166            })
167            .or_insert_with(|| {
168                let mut map: HashMap<Bytes, i64> = HashMap::new();
169                map.insert(name.clone().into(), amount);
170                map
171            });
172
173        self.mint = Some(MintAssets(mint));
174
175        Ok(self)
176    }
177
178    /// Remove the mint/burn entry for `(policy, name)`.
179    pub fn remove_mint_asset(mut self, policy: Hash<28>, name: Vec<u8>) -> Self {
180        let mut mint = if let Some(mint) = self.mint {
181            mint.0
182        } else {
183            return self;
184        };
185
186        if let Some(assets) = mint.get_mut(&Hash28(*policy)) {
187            assets.remove(&name.into());
188            if assets.is_empty() {
189                mint.remove(&Hash28(*policy));
190            }
191        }
192
193        self.mint = Some(MintAssets(mint));
194
195        self
196    }
197
198    /// Set the earliest slot at which the transaction becomes valid.
199    pub fn valid_from_slot(mut self, slot: u64) -> Self {
200        self.valid_from_slot = Some(slot);
201        self
202    }
203
204    /// Clear the lower validity bound.
205    pub fn clear_valid_from_slot(mut self) -> Self {
206        self.valid_from_slot = None;
207        self
208    }
209
210    /// Set the slot at which the transaction becomes invalid (TTL).
211    pub fn invalid_from_slot(mut self, slot: u64) -> Self {
212        self.invalid_from_slot = Some(slot);
213        self
214    }
215
216    /// Clear the TTL.
217    pub fn clear_invalid_from_slot(mut self) -> Self {
218        self.invalid_from_slot = None;
219        self
220    }
221
222    /// Set the network id (0 = testnet, 1 = mainnet).
223    pub fn network_id(mut self, id: u8) -> Self {
224        self.network_id = Some(id);
225        self
226    }
227
228    /// Clear the network id.
229    pub fn clear_network_id(mut self) -> Self {
230        self.network_id = None;
231        self
232    }
233
234    /// Append a collateral input.
235    pub fn collateral_input(mut self, input: Input) -> Self {
236        let mut coll_ins = self.collateral_inputs.unwrap_or_default();
237        coll_ins.push(input);
238        self.collateral_inputs = Some(coll_ins);
239        self
240    }
241
242    /// Remove a previously added collateral input.
243    pub fn remove_collateral_input(mut self, input: Input) -> Self {
244        let mut coll_ins = self.collateral_inputs.unwrap_or_default();
245        coll_ins.retain(|x| *x != input);
246        self.collateral_inputs = Some(coll_ins);
247        self
248    }
249
250    /// Set the collateral-return output.
251    pub fn collateral_output(mut self, output: Output) -> Self {
252        self.collateral_output = Some(output);
253        self
254    }
255
256    /// Clear the collateral-return output.
257    pub fn clear_collateral_output(mut self) -> Self {
258        self.collateral_output = None;
259        self
260    }
261
262    /// Add a required-signer key hash.
263    pub fn disclosed_signer(mut self, pub_key_hash: Hash<28>) -> Self {
264        let mut disclosed_signers = self.disclosed_signers.unwrap_or_default();
265        disclosed_signers.push(Hash28(*pub_key_hash));
266        self.disclosed_signers = Some(disclosed_signers);
267        self
268    }
269
270    /// Remove a previously added required-signer key hash.
271    pub fn remove_disclosed_signer(mut self, pub_key_hash: Hash<28>) -> Self {
272        let mut disclosed_signers = self.disclosed_signers.unwrap_or_default();
273        disclosed_signers.retain(|x| *x != Hash28(*pub_key_hash));
274        self.disclosed_signers = Some(disclosed_signers);
275        self
276    }
277
278    /// Attach a native or Plutus script. The script is keyed by its
279    /// language-tagged Blake2b-224 hash.
280    pub fn script(mut self, language: ScriptKind, bytes: Vec<u8>) -> Self {
281        let mut scripts = self.scripts.unwrap_or_default();
282
283        let hash = match language {
284            ScriptKind::Native => Hasher::<224>::hash_tagged(bytes.as_ref(), 0),
285            ScriptKind::PlutusV1 => Hasher::<224>::hash_tagged(bytes.as_ref(), 1),
286            ScriptKind::PlutusV2 => Hasher::<224>::hash_tagged(bytes.as_ref(), 2),
287            ScriptKind::PlutusV3 => Hasher::<224>::hash_tagged(bytes.as_ref(), 3),
288        };
289
290        scripts.insert(
291            Hash28(*hash),
292            Script {
293                kind: language,
294                bytes: bytes.into(),
295            },
296        );
297
298        self.scripts = Some(scripts);
299        self
300    }
301
302    /// Remove the script with the given hash.
303    pub fn remove_script_by_hash(mut self, script_hash: Hash<28>) -> Self {
304        let mut scripts = self.scripts.unwrap_or_default();
305
306        scripts.remove(&Hash28(*script_hash));
307
308        self.scripts = Some(scripts);
309        self
310    }
311
312    /// Attach a Plutus datum keyed by its Blake2b-256 hash.
313    pub fn datum(mut self, datum: Vec<u8>) -> Self {
314        let mut datums = self.datums.unwrap_or_default();
315
316        let hash = Hasher::<256>::hash_cbor(&datum);
317
318        datums.insert(Bytes32(*hash), datum.into());
319        self.datums = Some(datums);
320        self
321    }
322
323    /// Remove a previously attached datum identified by its raw bytes.
324    pub fn remove_datum(mut self, datum: Vec<u8>) -> Self {
325        let mut datums = self.datums.unwrap_or_default();
326
327        let hash = Hasher::<256>::hash_cbor(&datum);
328
329        datums.remove(&Bytes32(*hash));
330        self.datums = Some(datums);
331        self
332    }
333
334    /// Remove a previously attached datum identified by its hash.
335    pub fn remove_datum_by_hash(mut self, datum_hash: Hash<32>) -> Self {
336        let mut datums = self.datums.unwrap_or_default();
337
338        datums.remove(&Bytes32(*datum_hash));
339        self.datums = Some(datums);
340        self
341    }
342
343    /// Replace the Plutus language-views map used to compute the script-data hash.
344    pub fn language_views(mut self, views: pallas_primitives::conway::LanguageViews) -> Self {
345        self.language_views = Some(views);
346        self
347    }
348
349    /// Add or replace the cost model for a single Plutus language version.
350    /// Native scripts are a no-op.
351    pub fn add_language(mut self, plutus_version: ScriptKind, cost_model: Vec<i64>) -> Self {
352        let version = match plutus_version {
353            ScriptKind::PlutusV1 => 0,
354            ScriptKind::PlutusV2 => 1,
355            ScriptKind::PlutusV3 => 2,
356            ScriptKind::Native => return self,
357        };
358        let mut map = self
359            .language_views
360            .as_ref()
361            .map(|v| v.0.clone())
362            .unwrap_or_default();
363        map.insert(version, cost_model);
364        self.language_views = Some(pallas_primitives::conway::LanguageViews(map));
365        self
366    }
367
368    /// Attach a spend redeemer targeting `input`. Pass `ex_units = None` to
369    /// have the builder compute them later.
370    pub fn add_spend_redeemer(
371        mut self,
372        input: Input,
373        plutus_data: Vec<u8>,
374        ex_units: Option<ExUnits>,
375    ) -> Self {
376        let mut rdmrs = self.redeemers.map(|x| x.0).unwrap_or_default();
377
378        rdmrs.insert(
379            RedeemerPurpose::Spend(input),
380            (plutus_data.into(), ex_units),
381        );
382
383        self.redeemers = Some(Redeemers(rdmrs));
384
385        self
386    }
387
388    /// Remove the spend redeemer targeting `input`.
389    pub fn remove_spend_redeemer(mut self, input: Input) -> Self {
390        let mut rdmrs = self.redeemers.map(|x| x.0).unwrap_or_default();
391
392        rdmrs.remove(&RedeemerPurpose::Spend(input));
393
394        self.redeemers = Some(Redeemers(rdmrs));
395
396        self
397    }
398
399    /// Attach a mint redeemer targeting `policy`. Pass `ex_units = None` to
400    /// have the builder compute them later.
401    pub fn add_mint_redeemer(
402        mut self,
403        policy: Hash<28>,
404        plutus_data: Vec<u8>,
405        ex_units: Option<ExUnits>,
406    ) -> Self {
407        let mut rdmrs = self.redeemers.map(|x| x.0).unwrap_or_default();
408
409        rdmrs.insert(
410            RedeemerPurpose::Mint(Hash28(*policy)),
411            (plutus_data.into(), ex_units),
412        );
413
414        self.redeemers = Some(Redeemers(rdmrs));
415
416        self
417    }
418
419    /// Remove the mint redeemer targeting `policy`.
420    pub fn remove_mint_redeemer(mut self, policy: Hash<28>) -> Self {
421        let mut rdmrs = self.redeemers.map(|x| x.0).unwrap_or_default();
422
423        rdmrs.remove(&RedeemerPurpose::Mint(Hash28(*policy)));
424
425        self.redeemers = Some(Redeemers(rdmrs));
426
427        self
428    }
429
430    /// Override the assumed signature count used in fee estimation.
431    pub fn signature_amount_override(mut self, amount: u8) -> Self {
432        self.signature_amount_override = Some(amount);
433        self
434    }
435
436    /// Clear the signature-count override.
437    pub fn clear_signature_amount_override(mut self) -> Self {
438        self.signature_amount_override = None;
439        self
440    }
441
442    /// Set the address receiving change when the builder balances the transaction.
443    pub fn change_address(mut self, address: PallasAddress) -> Self {
444        self.change_address = Some(Address(address));
445        self
446    }
447
448    /// Clear the change address.
449    pub fn clear_change_address(mut self) -> Self {
450        self.change_address = None;
451        self
452    }
453
454    /// Attach auxiliary data parsed from raw CBOR. Invalid CBOR is silently ignored.
455    pub fn add_auxiliary_data(mut self, data: Vec<u8>) -> Self {
456        if let Ok(aux) = minicbor::decode::<AuxiliaryData>(data.as_ref()) {
457            self.auxiliary_data = Some(aux);
458        }
459        self
460    }
461
462    /// Clear any attached auxiliary data.
463    pub fn clear_auxiliary_data(mut self) -> Self {
464        self.auxiliary_data = None;
465        self
466    }
467}
468
469// TODO: Don't want our wrapper types in fields public
470/// Reference to a single transaction output (consumed or referenced).
471#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Hash, Clone)]
472pub struct Input {
473    /// Hash of the transaction that produced the output.
474    pub tx_hash: TxHash,
475    /// Index of the output within that transaction.
476    pub txo_index: u64,
477}
478
479impl Input {
480    /// Build an input from a transaction hash and output index.
481    pub fn new(tx_hash: Hash<32>, txo_index: u64) -> Self {
482        Self {
483            tx_hash: Bytes32(*tx_hash),
484            txo_index,
485        }
486    }
487}
488
489// TODO: Don't want our wrapper types in fields public
490/// Transaction output being produced.
491#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
492pub struct Output {
493    /// Destination address.
494    pub address: Address,
495    /// Lovelace amount.
496    pub lovelace: u64,
497    /// Optional native-token bundle paired with the lovelace.
498    pub assets: Option<OutputAssets>,
499    /// Optional datum (inline data or hash) attached to the output.
500    pub datum: Option<Datum>,
501    /// Optional script reference attached to the output (CIP-33).
502    pub script: Option<Script>,
503}
504
505impl Output {
506    /// Build an output paying `lovelace` to `address`.
507    pub fn new(address: PallasAddress, lovelace: u64) -> Self {
508        Self {
509            address: Address(address),
510            lovelace,
511            assets: None,
512            datum: None,
513            script: None,
514        }
515    }
516
517    /// Add (or accumulate) a quantity of `(policy, name)` to this output.
518    /// Fails if the asset name exceeds 32 bytes.
519    pub fn add_asset(
520        mut self,
521        policy: Hash<28>,
522        name: Vec<u8>,
523        amount: u64,
524    ) -> Result<Self, TxBuilderError> {
525        if name.len() > 32 {
526            return Err(TxBuilderError::AssetNameTooLong);
527        }
528
529        let mut assets = self.assets.map(|x| x.0).unwrap_or_default();
530
531        assets
532            .entry(Hash28(*policy))
533            .and_modify(|policy_map| {
534                policy_map
535                    .entry(name.clone().into())
536                    .and_modify(|asset_map| {
537                        *asset_map += amount;
538                    })
539                    .or_insert(amount);
540            })
541            .or_insert_with(|| {
542                let mut map: HashMap<Bytes, u64> = HashMap::new();
543                map.insert(name.clone().into(), amount);
544                map
545            });
546
547        self.assets = Some(OutputAssets(assets));
548
549        Ok(self)
550    }
551
552    /// Attach an inline datum (CIP-32) to this output.
553    pub fn set_inline_datum(mut self, plutus_data: Vec<u8>) -> Self {
554        self.datum = Some(Datum {
555            kind: DatumKind::Inline,
556            bytes: plutus_data.into(),
557        });
558
559        self
560    }
561
562    /// Attach a datum hash to this output (datum body provided separately).
563    pub fn set_datum_hash(mut self, datum_hash: Hash<32>) -> Self {
564        self.datum = Some(Datum {
565            kind: DatumKind::Hash,
566            bytes: datum_hash.to_vec().into(),
567        });
568
569        self
570    }
571
572    /// Attach an inline script reference (CIP-33) to this output.
573    pub fn set_inline_script(mut self, language: ScriptKind, bytes: Vec<u8>) -> Self {
574        self.script = Some(Script {
575            kind: language,
576            bytes: bytes.into(),
577        });
578
579        self
580    }
581}
582
583/// Native-token bundle attached to a transaction output.
584#[derive(PartialEq, Eq, Debug, Clone, Default)]
585pub struct OutputAssets(HashMap<PolicyId, HashMap<AssetName, u64>>);
586
587impl Deref for OutputAssets {
588    type Target = HashMap<PolicyId, HashMap<Bytes, u64>>;
589
590    fn deref(&self) -> &Self::Target {
591        &self.0
592    }
593}
594
595impl OutputAssets {
596    /// Build an [`OutputAssets`] from a pre-constructed policy → asset map.
597    pub fn from_map(map: HashMap<PolicyId, HashMap<Bytes, u64>>) -> Self {
598        Self(map)
599    }
600}
601
602/// Mint/burn bundle attached to a transaction (signed quantities).
603#[derive(PartialEq, Eq, Debug, Clone, Default)]
604pub struct MintAssets(HashMap<PolicyId, HashMap<AssetName, i64>>);
605
606impl Deref for MintAssets {
607    type Target = HashMap<PolicyId, HashMap<Bytes, i64>>;
608
609    fn deref(&self) -> &Self::Target {
610        &self.0
611    }
612}
613
614impl MintAssets {
615    /// Create an empty mint bundle.
616    pub fn new() -> Self {
617        MintAssets(HashMap::new())
618    }
619
620    /// Build a [`MintAssets`] from a pre-constructed policy → asset map.
621    pub fn from_map(map: HashMap<PolicyId, HashMap<Bytes, i64>>) -> Self {
622        Self(map)
623    }
624}
625
626/// Discriminator selecting a script language.
627#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy)]
628#[serde(rename_all = "snake_case")]
629pub enum ScriptKind {
630    /// Cardano native script (timelock and signature combinators).
631    Native,
632    /// Plutus V1 script.
633    PlutusV1,
634    /// Plutus V2 script.
635    PlutusV2,
636    /// Plutus V3 script.
637    PlutusV3,
638}
639
640/// A native or Plutus script and its language.
641#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
642pub struct Script {
643    /// Script language.
644    pub kind: ScriptKind,
645    /// Serialized script bytes.
646    pub bytes: ScriptBytes,
647}
648
649impl Script {
650    /// Build a script from its language tag and bytes.
651    pub fn new(kind: ScriptKind, bytes: Vec<u8>) -> Self {
652        Self {
653            kind,
654            bytes: bytes.into(),
655        }
656    }
657}
658
659/// Discriminator for how a datum is referenced from an output.
660#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
661#[serde(rename_all = "snake_case")]
662pub enum DatumKind {
663    /// `bytes` is the 32-byte Blake2b-256 hash of the datum.
664    Hash,
665    /// `bytes` is the inline datum body (CIP-32).
666    Inline,
667}
668
669/// Datum attached to an output, either by hash or inline.
670#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
671pub struct Datum {
672    /// Whether `bytes` holds a hash or the inline body.
673    pub kind: DatumKind,
674    /// Hash or inline payload, per [`DatumKind`].
675    pub bytes: DatumBytes,
676}
677
678/// Target a Plutus redeemer applies to.
679#[derive(PartialEq, Eq, Hash, Debug, Clone)]
680pub enum RedeemerPurpose {
681    /// Spend redeemer targeting a specific input.
682    Spend(Input),
683    /// Mint redeemer targeting a specific minting policy.
684    Mint(PolicyId),
685    // Reward TODO
686    // Cert TODO
687}
688
689/// Plutus script execution budget.
690#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
691pub struct ExUnits {
692    /// Memory units consumed.
693    pub mem: u64,
694    /// CPU step units consumed.
695    pub steps: u64,
696}
697
698/// Plutus redeemers keyed by their purpose.
699#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Default, Clone)]
700pub struct Redeemers(HashMap<RedeemerPurpose, (Bytes, Option<ExUnits>)>);
701
702impl Deref for Redeemers {
703    type Target = HashMap<RedeemerPurpose, (Bytes, Option<ExUnits>)>;
704
705    fn deref(&self) -> &Self::Target {
706        &self.0
707    }
708}
709
710impl Redeemers {
711    /// Build a [`Redeemers`] from a pre-constructed purpose → `(datum, ex_units)` map.
712    pub fn from_map(map: HashMap<RedeemerPurpose, (Bytes, Option<ExUnits>)>) -> Self {
713        Self(map)
714    }
715}
716
717/// Newtype wrapper around [`pallas_addresses::Address`] usable in serde contexts.
718#[derive(PartialEq, Eq, Debug, Clone)]
719pub struct Address(pub PallasAddress);
720
721impl Deref for Address {
722    type Target = PallasAddress;
723
724    fn deref(&self) -> &Self::Target {
725        &self.0
726    }
727}
728
729impl From<PallasAddress> for Address {
730    fn from(value: PallasAddress) -> Self {
731        Self(value)
732    }
733}
734
735/// Era the builder targeted when producing a [`BuiltTransaction`].
736#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
737#[serde(rename_all = "snake_case")]
738pub enum BuilderEra {
739    /// Babbage-era output shape.
740    Babbage,
741    /// Conway-era output shape.
742    Conway,
743}
744
745/// Anything that can produce Ed25519 signatures used by [`BuiltTransaction::sign`].
746pub trait Ed25519Signer {
747    /// Return the public key paired with this signer.
748    fn public_key(&self) -> ed25519::PublicKey;
749    /// Sign `msg` with this signer.
750    fn sign<T: AsRef<[u8]>>(&self, msg: T) -> ed25519::Signature;
751}
752
753impl Ed25519Signer for ed25519::SecretKey {
754    fn public_key(&self) -> ed25519::PublicKey {
755        self.public_key()
756    }
757
758    fn sign<T: AsRef<[u8]>>(&self, msg: T) -> ed25519::Signature {
759        self.sign(msg)
760    }
761}
762
763impl Ed25519Signer for ed25519::SecretKeyExtended {
764    fn public_key(&self) -> ed25519::PublicKey {
765        self.public_key()
766    }
767
768    fn sign<T: AsRef<[u8]>>(&self, msg: T) -> ed25519::Signature {
769        self.sign(msg)
770    }
771}
772
773/// A fully built (and possibly signed) transaction ready to submit.
774#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
775pub struct BuiltTransaction {
776    /// Schema version of this built document (matches the source [`StagingTransaction`]).
777    pub version: String,
778    /// Era the transaction was built for.
779    pub era: BuilderEra,
780    /// Lifecycle state — typically `TransactionStatus::Built`.
781    pub status: TransactionStatus,
782    /// Hash of the transaction body (the message signers attest to).
783    pub tx_hash: TxHash,
784    /// CBOR-encoded transaction bytes.
785    pub tx_bytes: Bytes,
786    /// Map of public-key → signature for each attached witness.
787    pub signatures: Option<HashMap<PublicKey, Signature>>,
788}
789
790impl BuiltTransaction {
791    /// Sign this transaction with `private_key` and embed the witness in
792    /// `tx_bytes`. Calling multiple times accumulates witnesses.
793    pub fn sign<K: Ed25519Signer>(mut self, private_key: &K) -> Result<Self, TxBuilderError> {
794        let pubkey: [u8; 32] = private_key
795            .public_key()
796            .as_ref()
797            .try_into()
798            .map_err(|_| TxBuilderError::MalformedKey)?;
799
800        let signature: [u8; ed25519::Signature::SIZE] = private_key
801            .sign(self.tx_hash.0)
802            .as_ref()
803            .try_into()
804            .unwrap();
805
806        match self.era {
807            BuilderEra::Conway => {
808                let mut new_sigs = self.signatures.unwrap_or_default();
809
810                new_sigs.insert(Bytes32(pubkey), Bytes64(signature));
811
812                self.signatures = Some(new_sigs);
813
814                // TODO: chance for serialisation round trip issues?
815                let mut tx = conway::Tx::decode_fragment(&self.tx_bytes.0)
816                    .map_err(|_| TxBuilderError::CorruptedTxBytes)?;
817
818                let mut vkey_witnesses = tx
819                    .transaction_witness_set
820                    .vkeywitness
821                    .as_ref()
822                    .map(|x| x.clone().to_vec())
823                    .unwrap_or_default();
824
825                vkey_witnesses.push(conway::VKeyWitness {
826                    vkey: Vec::from(pubkey.as_ref()).into(),
827                    signature: Vec::from(signature.as_ref()).into(),
828                });
829
830                tx.transaction_witness_set.vkeywitness =
831                    Some(NonEmptySet::from_vec(vkey_witnesses).unwrap());
832
833                self.tx_bytes = tx.encode_fragment().unwrap().into();
834            }
835            _ => return Err(TxBuilderError::UnsupportedEra),
836        }
837
838        Ok(self)
839    }
840
841    /// Embed a signature produced out-of-band. Useful for HSM / hardware-wallet flows.
842    pub fn add_signature(
843        mut self,
844        pub_key: ed25519::PublicKey,
845        signature: [u8; 64],
846    ) -> Result<Self, TxBuilderError> {
847        match self.era {
848            BuilderEra::Conway => {
849                let mut new_sigs = self.signatures.unwrap_or_default();
850
851                new_sigs.insert(
852                    Bytes32(
853                        pub_key
854                            .as_ref()
855                            .try_into()
856                            .map_err(|_| TxBuilderError::MalformedKey)?,
857                    ),
858                    Bytes64(signature),
859                );
860
861                self.signatures = Some(new_sigs);
862
863                // TODO: chance for serialisation round trip issues?
864                let mut tx = conway::Tx::decode_fragment(&self.tx_bytes.0)
865                    .map_err(|_| TxBuilderError::CorruptedTxBytes)?;
866
867                let mut vkey_witnesses = tx
868                    .transaction_witness_set
869                    .vkeywitness
870                    .as_ref()
871                    .map(|x| x.clone().to_vec())
872                    .unwrap_or_default();
873
874                vkey_witnesses.push(conway::VKeyWitness {
875                    vkey: Vec::from(pub_key.as_ref()).into(),
876                    signature: Vec::from(signature.as_ref()).into(),
877                });
878
879                tx.transaction_witness_set.vkeywitness =
880                    Some(NonEmptySet::from_vec(vkey_witnesses).unwrap());
881
882                self.tx_bytes = tx.encode_fragment().unwrap().into();
883            }
884            _ => return Err(TxBuilderError::UnsupportedEra),
885        }
886
887        Ok(self)
888    }
889
890    /// Remove the witness attached for `pub_key`, if any.
891    pub fn remove_signature(mut self, pub_key: ed25519::PublicKey) -> Result<Self, TxBuilderError> {
892        match self.era {
893            BuilderEra::Conway => {
894                let mut new_sigs = self.signatures.unwrap_or_default();
895
896                let pk = Bytes32(
897                    pub_key
898                        .as_ref()
899                        .try_into()
900                        .map_err(|_| TxBuilderError::MalformedKey)?,
901                );
902
903                new_sigs.remove(&pk);
904
905                self.signatures = Some(new_sigs);
906
907                // TODO: chance for serialisation round trip issues?
908                let mut tx = conway::Tx::decode_fragment(&self.tx_bytes.0)
909                    .map_err(|_| TxBuilderError::CorruptedTxBytes)?;
910
911                let mut vkey_witnesses = tx
912                    .transaction_witness_set
913                    .vkeywitness
914                    .as_ref()
915                    .map(|x| x.clone().to_vec())
916                    .unwrap_or_default();
917
918                vkey_witnesses.retain(|x| *x.vkey != pk.0.to_vec());
919
920                tx.transaction_witness_set.vkeywitness =
921                    Some(NonEmptySet::from_vec(vkey_witnesses).unwrap());
922
923                self.tx_bytes = tx.encode_fragment().unwrap().into();
924            }
925            _ => return Err(TxBuilderError::UnsupportedEra),
926        }
927
928        Ok(self)
929    }
930}