Skip to main content

tx3_sdk/
facade.rs

1//! Ergonomic facade for the full TX3 lifecycle.
2//!
3//! This module provides a high-level API that covers invocation, resolution,
4//! signing, submission, and status polling.
5
6use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8use std::time::Duration;
9
10use serde::Deserialize;
11use serde_json::Value;
12use thiserror::Error;
13
14use crate::core::{ArgMap, BytesEnvelope, EnvMap, TirEnvelope};
15use crate::tii::Protocol;
16use crate::trp::{self, ResolveParams, SubmitParams, TxStage, TxStatus, TxWitness};
17
18#[derive(Clone)]
19struct SignerParty {
20    name: String,
21    address: String,
22    signer: Arc<dyn Signer + Send + Sync>,
23}
24
25/// Error type for facade operations.
26#[derive(Debug, Error)]
27pub enum Error {
28    /// Error originating from TII operations.
29    #[error(transparent)]
30    Tii(#[from] crate::tii::Error),
31
32    /// Error originating from TRP operations.
33    #[error(transparent)]
34    Trp(#[from] crate::trp::Error),
35
36    /// A transaction name was not declared by the protocol.
37    #[error("unknown transaction: {0}")]
38    UnknownTx(String),
39
40    /// A profile name was not declared by the protocol.
41    #[error("unknown profile: {0}")]
42    UnknownProfile(String),
43
44    /// A party name was not declared by the protocol.
45    #[error("unknown party: {0}")]
46    UnknownParty(String),
47
48    /// The builder was finalized without a TRP endpoint.
49    #[error("TRP endpoint not configured")]
50    MissingTrpEndpoint,
51
52    /// Signer failed to produce a witness.
53    #[error("signer error: {0}")]
54    Signer(#[source] Box<dyn std::error::Error + Send + Sync>),
55
56    /// Submitted hash does not match the resolved hash.
57    #[error("submit hash mismatch: expected {expected}, got {received}")]
58    SubmitHashMismatch { expected: String, received: String },
59
60    /// Transaction failed to reach confirmation.
61    #[error("tx {hash} failed with stage {stage:?}")]
62    FinalizedFailed { hash: String, stage: TxStage },
63
64    /// Transaction did not reach confirmation within the polling window.
65    #[error("tx {hash} not confirmed after {attempts} attempts (delay {delay:?})")]
66    FinalizedTimeout {
67        hash: String,
68        attempts: u32,
69        delay: Duration,
70    },
71}
72
73/// Configuration for check-status polling.
74///
75/// Used by `wait_for_confirmed` and `wait_for_finalized`.
76#[derive(Debug, Clone)]
77pub struct PollConfig {
78    /// Number of attempts before timing out.
79    pub attempts: u32,
80    /// Delay between attempts.
81    pub delay: Duration,
82}
83
84impl Default for PollConfig {
85    fn default() -> Self {
86        Self {
87            attempts: 20,
88            delay: Duration::from_secs(5),
89        }
90    }
91}
92
93/// Inputs passed to a [`Signer`] for each sign call.
94///
95/// Carries both the bound tx hash and the full hex-encoded tx CBOR. Hash-based
96/// signers (Cardano, Ed25519) read `tx_hash_hex`; tx-based signers (e.g. wallet
97/// adapters that need the full tx body) read `tx_cbor_hex`. The SDK always
98/// populates both fields.
99#[derive(Debug, Clone)]
100pub struct SignRequest {
101    /// Hex-encoded tx hash bound to this signing call.
102    pub tx_hash_hex: String,
103    /// Hex-encoded full tx CBOR.
104    pub tx_cbor_hex: String,
105}
106
107/// A signer capable of producing TRP witnesses.
108///
109/// Signers are address-aware and must return the address they correspond to.
110pub trait Signer: Send + Sync {
111    /// Returns the address associated with this signer.
112    fn address(&self) -> &str;
113
114    /// Signs the transaction described by `request`.
115    fn sign(
116        &self,
117        request: &SignRequest,
118    ) -> Result<TxWitness, Box<dyn std::error::Error + Send + Sync>>;
119}
120
121/// A party referenced by the protocol.
122#[derive(Clone)]
123pub enum Party {
124    /// Read-only party with a known address.
125    Address(String),
126    /// Party capable of signing transactions.
127    Signer {
128        /// Party address (used for invocation args).
129        address: String,
130        /// Signer implementation.
131        signer: Arc<dyn Signer + Send + Sync>,
132    },
133}
134
135impl Party {
136    /// Creates a read-only party from an address.
137    pub fn address(address: impl Into<String>) -> Self {
138        Party::Address(address.into())
139    }
140
141    /// Creates a signer party from a signer.
142    ///
143    /// The party address is taken from the signer itself.
144    ///
145    /// # Example
146    ///
147    /// ```rust
148    /// use tx3_sdk::{CardanoSigner, Party};
149    ///
150    /// let signer = CardanoSigner::from_hex("addr_test1...", "deadbeef...")?;
151    /// let party = Party::signer(signer);
152    /// # Ok::<(), tx3_sdk::Error>(())
153    /// ```
154    pub fn signer(signer: impl Signer + 'static) -> Self {
155        Party::Signer {
156            address: signer.address().to_string(),
157            signer: Arc::new(signer),
158        }
159    }
160
161    fn address_value(&self) -> &str {
162        match self {
163            Party::Address(address) => address,
164            Party::Signer { address, .. } => address,
165        }
166    }
167
168    fn signer_party(&self, name: &str) -> Option<SignerParty> {
169        match self {
170            Party::Signer { address, signer } => Some(SignerParty {
171                name: name.to_string(),
172                address: address.clone(),
173                signer: Arc::clone(signer),
174            }),
175            _ => None,
176        }
177    }
178}
179
180/// A named profile baked into a client: environment values and party
181/// addresses keyed by name.
182///
183/// Produced either by deconstructing a loaded [`Protocol`] inside
184/// [`Tx3ClientBuilder::from_protocol`] or by parsing the per-profile JSON
185/// blob a generated codegen client embeds (via `serde_json::from_str` —
186/// `Profile` derives `Deserialize`).
187#[derive(Debug, Clone, Default, Deserialize)]
188pub struct Profile {
189    /// Environment values applied to every transaction under this profile.
190    #[serde(default)]
191    pub environment: EnvMap,
192    /// Party addresses applied to every transaction under this profile.
193    #[serde(default)]
194    pub parties: HashMap<String, String>,
195}
196
197/// High-level client over a TX3 protocol.
198///
199/// Holds the deconstructed protocol parts — per-transaction TIR envelopes,
200/// named profiles, the set of declared party names — plus the runtime state
201/// (TRP client, bound parties, selected profile and env overrides).
202///
203/// Construct one through [`Tx3ClientBuilder`], obtained via
204/// [`Protocol::client`]: profile selection and party/env binding happen on
205/// the builder, and `build()` performs all fallible validation.
206#[derive(Clone)]
207pub struct Tx3Client {
208    transactions: HashMap<String, TirEnvelope>,
209    known_parties: HashSet<String>,
210    trp: trp::Client,
211    bound_parties: HashMap<String, Party>,
212    selected_profile: Option<Profile>,
213    env_overrides: EnvMap,
214}
215
216impl Tx3Client {
217    /// Constructs a client from already-deconstructed protocol parts.
218    ///
219    /// Crate-internal entry used by [`Tx3ClientBuilder::build`]. External
220    /// callers go through the builder.
221    pub(crate) fn from_parts(
222        transactions: HashMap<String, TirEnvelope>,
223        known_parties: HashSet<String>,
224        trp: trp::Client,
225        bound_parties: HashMap<String, Party>,
226        selected_profile: Option<Profile>,
227        env_overrides: EnvMap,
228    ) -> Self {
229        let known_parties = known_parties
230            .into_iter()
231            .map(|name| name.to_lowercase())
232            .collect();
233        Self {
234            transactions,
235            known_parties,
236            trp,
237            bound_parties,
238            selected_profile,
239            env_overrides,
240        }
241    }
242
243    /// Binds a party (signer or read-only address) by name after the client
244    /// has been built. Useful for late binding when, e.g., a user logs in
245    /// after the client is already in scope.
246    ///
247    /// Overrides any address the selected profile declared for the same name.
248    ///
249    /// # Errors
250    ///
251    /// Returns [`Error::UnknownParty`] if `name` is not a party declared by
252    /// the protocol.
253    pub fn with_party(
254        mut self,
255        name: impl Into<String>,
256        party: Party,
257    ) -> Result<Self, Error> {
258        let name = name.into().to_lowercase();
259        if !self.known_parties.contains(&name) {
260            return Err(Error::UnknownParty(name));
261        }
262        self.bound_parties.insert(name, party);
263        Ok(self)
264    }
265
266    /// Binds a party without validating the name against the protocol's
267    /// declared parties. Intended for codegen-generated wrappers — see
268    /// [`Tx3ClientBuilder::with_party_unchecked`]. Hand-written code SHOULD
269    /// use [`Tx3Client::with_party`].
270    pub fn with_party_unchecked(
271        mut self,
272        name: impl Into<String>,
273        party: Party,
274    ) -> Self {
275        self.bound_parties
276            .insert(name.into().to_lowercase(), party);
277        self
278    }
279
280    /// Binds multiple parties at once. See [`Tx3Client::with_party`].
281    pub fn with_parties<I, K>(mut self, parties: I) -> Result<Self, Error>
282    where
283        I: IntoIterator<Item = (K, Party)>,
284        K: Into<String>,
285    {
286        for (name, party) in parties {
287            self = self.with_party(name, party)?;
288        }
289        Ok(self)
290    }
291
292    /// Starts building a transaction invocation.
293    ///
294    /// # Errors
295    ///
296    /// Returns [`Error::UnknownTx`] if `name` is not a transaction declared
297    /// by the protocol.
298    pub fn tx(&self, name: impl Into<String>) -> Result<TxBuilder, Error> {
299        let name = name.into();
300        let tir = self
301            .transactions
302            .get(&name)
303            .cloned()
304            .ok_or(Error::UnknownTx(name))?;
305
306        Ok(TxBuilder::new(tir, self.trp.clone())
307            .env(self.env())
308            .parties(self.merged_parties()))
309    }
310
311    fn env(&self) -> EnvMap {
312        let mut env = self
313            .selected_profile
314            .as_ref()
315            .map(|profile| profile.environment.clone())
316            .unwrap_or_default();
317        for (key, value) in &self.env_overrides {
318            env.insert(key.clone(), value.clone());
319        }
320        env
321    }
322
323    fn merged_parties(&self) -> HashMap<String, Party> {
324        let mut merged = HashMap::new();
325        if let Some(profile) = &self.selected_profile {
326            for (name, address) in &profile.parties {
327                merged.insert(name.to_lowercase(), Party::address(address.clone()));
328            }
329        }
330        for (name, party) in &self.bound_parties {
331            merged.insert(name.clone(), party.clone());
332        }
333        merged
334    }
335}
336
337/// Builder for [`Tx3Client`].
338///
339/// Obtained via [`Protocol::client`]. All fallible validation — verifying
340/// that the selected profile exists, that every bound party is declared by
341/// the protocol — happens in [`Tx3ClientBuilder::build`]. Setters never
342/// return `Result`, so chains stay fluent.
343///
344/// # Example
345///
346/// ```ignore
347/// use tx3_sdk::tii::Protocol;
348/// use tx3_sdk::{Party};
349///
350/// let client = Protocol::from_file("protocol.tii")?
351///     .client()
352///     .trp_endpoint("https://trp.example")
353///     .with_profile("preprod")
354///     .with_party("sender", Party::address("addr_test1..."))
355///     .build()?;
356/// ```
357pub struct Tx3ClientBuilder {
358    transactions: HashMap<String, TirEnvelope>,
359    profiles: HashMap<String, Profile>,
360    known_parties: HashSet<String>,
361    trp_options: Option<trp::ClientOptions>,
362    profile: Option<String>,
363    parties: HashMap<String, Party>,
364    unchecked_parties: HashMap<String, Party>,
365    env_overrides: EnvMap,
366}
367
368impl Tx3ClientBuilder {
369    /// Seeds a builder with already-deconstructed protocol fragments. This is
370    /// the entry point used by codegen-generated bindings, which embed only
371    /// the runtime essentials at codegen time (per-tx TIR envelopes,
372    /// per-profile environment + party-address maps, declared party names)
373    /// and avoid carrying the rest of the TII document into the generated
374    /// crate.
375    pub fn from_parts(
376        transactions: HashMap<String, TirEnvelope>,
377        profiles: HashMap<String, Profile>,
378        known_parties: HashSet<String>,
379    ) -> Self {
380        let known_parties = known_parties
381            .into_iter()
382            .map(|name| name.to_lowercase())
383            .collect();
384        Self {
385            transactions,
386            profiles,
387            known_parties,
388            trp_options: None,
389            profile: None,
390            parties: HashMap::new(),
391            unchecked_parties: HashMap::new(),
392            env_overrides: EnvMap::new(),
393        }
394    }
395
396    pub(crate) fn from_protocol(protocol: Protocol) -> Self {
397        let transactions = protocol
398            .txs()
399            .iter()
400            .map(|(name, tx)| (name.clone(), tx.tir.clone()))
401            .collect();
402
403        let profiles = protocol
404            .profiles()
405            .iter()
406            .map(|(name, profile)| {
407                let environment =
408                    profile.environment.as_object().cloned().unwrap_or_default();
409                (
410                    name.clone(),
411                    Profile {
412                        environment,
413                        parties: profile.parties.clone(),
414                    },
415                )
416            })
417            .collect();
418
419        let known_parties = protocol.parties().keys().cloned().collect();
420
421        Self::from_parts(transactions, profiles, known_parties)
422    }
423
424    /// Sets the full TRP client options.
425    pub fn trp(mut self, opts: trp::ClientOptions) -> Self {
426        self.trp_options = Some(opts);
427        self
428    }
429
430    /// Sets the TRP endpoint URL (no headers). Overwrites any previously
431    /// supplied options.
432    pub fn trp_endpoint(mut self, url: impl Into<String>) -> Self {
433        self.trp_options = Some(trp::ClientOptions {
434            endpoint: url.into(),
435            headers: None,
436        });
437        self
438    }
439
440    /// Adds a header to the TRP client. Initializes the TRP options to an
441    /// empty endpoint if not yet set — callers must still supply an endpoint
442    /// via [`Tx3ClientBuilder::trp`] or [`Tx3ClientBuilder::trp_endpoint`].
443    pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
444        let opts = self.trp_options.get_or_insert_with(|| trp::ClientOptions {
445            endpoint: String::new(),
446            headers: None,
447        });
448        opts.headers
449            .get_or_insert_with(HashMap::new)
450            .insert(key.into(), value.into());
451        self
452    }
453
454    /// Selects a profile by name. Validated in `build()`.
455    pub fn with_profile(mut self, name: impl Into<String>) -> Self {
456        self.profile = Some(name.into());
457        self
458    }
459
460    /// Binds a party (signer or read-only address) by name. Validated in
461    /// `build()` against the protocol's declared parties.
462    pub fn with_party(mut self, name: impl Into<String>, party: Party) -> Self {
463        self.parties.insert(name.into().to_lowercase(), party);
464        self
465    }
466
467    /// Binds a party without validating the name against the protocol's
468    /// declared parties. The entry is carried straight through to the built
469    /// client.
470    ///
471    /// Intended for codegen-generated wrappers, which materialize one typed
472    /// setter per declared party — the name is baked in at codegen time, so
473    /// runtime validation would always pass and the embedded party-name set
474    /// can be omitted. Hand-written code SHOULD use [`Tx3ClientBuilder::with_party`].
475    pub fn with_party_unchecked(mut self, name: impl Into<String>, party: Party) -> Self {
476        self.unchecked_parties
477            .insert(name.into().to_lowercase(), party);
478        self
479    }
480
481    /// Binds multiple parties at once.
482    pub fn with_parties<I, K>(mut self, parties: I) -> Self
483    where
484        I: IntoIterator<Item = (K, Party)>,
485        K: Into<String>,
486    {
487        for (name, party) in parties {
488            self = self.with_party(name, party);
489        }
490        self
491    }
492
493    /// Sets a single environment value. Merged on top of the selected
494    /// profile's environment at resolve time (override wins).
495    pub fn with_env_value(
496        mut self,
497        key: impl Into<String>,
498        value: impl Into<Value>,
499    ) -> Self {
500        self.env_overrides.insert(key.into(), value.into());
501        self
502    }
503
504    /// Validates the builder state and materializes the [`Tx3Client`].
505    ///
506    /// # Errors
507    ///
508    /// - [`Error::Trp`] if no TRP endpoint was supplied.
509    /// - [`Error::UnknownProfile`] if the selected profile is not declared
510    ///   by the protocol.
511    /// - [`Error::UnknownParty`] if any bound party is not declared by the
512    ///   protocol.
513    pub fn build(self) -> Result<Tx3Client, Error> {
514        let trp_options = self.trp_options.ok_or(Error::MissingTrpEndpoint)?;
515        if trp_options.endpoint.is_empty() {
516            return Err(Error::MissingTrpEndpoint);
517        }
518
519        let selected_profile = match self.profile {
520            Some(name) => Some(
521                self.profiles
522                    .get(&name)
523                    .cloned()
524                    .ok_or(Error::UnknownProfile(name))?,
525            ),
526            None => None,
527        };
528
529        for name in self.parties.keys() {
530            if !self.known_parties.contains(name) {
531                return Err(Error::UnknownParty(name.clone()));
532            }
533        }
534
535        let trp = trp::Client::new(trp_options);
536
537        let mut bound_parties = self.parties;
538        bound_parties.extend(self.unchecked_parties);
539
540        Ok(Tx3Client::from_parts(
541            self.transactions,
542            self.known_parties,
543            trp,
544            bound_parties,
545            selected_profile,
546            self.env_overrides,
547        ))
548    }
549}
550
551/// Assembles the TRP resolve request shared by every [`TxBuilder`].
552///
553/// `env` (profile values, with any profile-declared party addresses already
554/// folded in), bound party addresses, and caller-supplied `args` are merged
555/// into a single argument map, in increasing order of precedence. The request
556/// `env` is left unset — TRP receives one argument map.
557fn build_resolve_params(
558    tir: TirEnvelope,
559    env: EnvMap,
560    parties: &HashMap<String, Party>,
561    args: ArgMap,
562) -> ResolveParams {
563    let mut merged = ArgMap::new();
564    merged.extend(env);
565    for (name, party) in parties {
566        merged.insert(
567            name.clone(),
568            Value::String(party.address_value().to_string()),
569        );
570    }
571    merged.extend(args);
572
573    ResolveParams {
574        tir,
575        args: merged,
576        env: None,
577    }
578}
579
580/// Builder for transaction invocation.
581///
582/// A builder is a TIR envelope plus the environment, arguments, and party
583/// bindings needed to resolve it. Generated codegen clients construct one via
584/// [`TxBuilder::new`]; the dynamic [`Tx3Client`] constructs one by adapting a
585/// loaded [`Protocol`]. Both drive an identical resolve path.
586pub struct TxBuilder {
587    tir: TirEnvelope,
588    env: EnvMap,
589    trp: trp::Client,
590    args: ArgMap,
591    parties: HashMap<String, Party>,
592}
593
594impl TxBuilder {
595    /// Creates a builder from a TIR envelope.
596    ///
597    /// This is the entry point used by generated codegen clients: they bake the
598    /// per-transaction TIR and profile data into the generated source at
599    /// codegen time and drive the full `resolve → sign → submit → wait`
600    /// lifecycle without loading a `.tii` file. Supply environment values with
601    /// [`TxBuilder::env`] and signer/address bindings with [`TxBuilder::parties`].
602    pub fn new(tir: TirEnvelope, trp: trp::Client) -> Self {
603        TxBuilder {
604            tir,
605            env: EnvMap::new(),
606            trp,
607            args: ArgMap::new(),
608            parties: HashMap::new(),
609        }
610    }
611
612    /// Sets the environment values applied to this transaction.
613    pub fn env(mut self, env: EnvMap) -> Self {
614        self.env = env;
615        self
616    }
617
618    /// Attaches party definitions (signers or read-only addresses).
619    ///
620    /// Names are matched case-insensitively. Later entries override earlier
621    /// ones with the same name.
622    pub fn parties(mut self, parties: HashMap<String, Party>) -> Self {
623        for (name, party) in parties {
624            self.parties.insert(name.to_lowercase(), party);
625        }
626        self
627    }
628
629    /// Adds a single argument (case-insensitive name).
630    pub fn arg(mut self, name: &str, value: impl Into<Value>) -> Self {
631        self.args.insert(name.to_lowercase(), value.into());
632        self
633    }
634
635    /// Adds multiple arguments (case-insensitive names).
636    pub fn args(mut self, args: ArgMap) -> Self {
637        for (key, value) in args {
638            self.args.insert(key.to_lowercase(), value);
639        }
640        self
641    }
642
643    /// Resolves the transaction using the TRP client.
644    pub async fn resolve(self) -> Result<ResolvedTx, Error> {
645        let TxBuilder {
646            tir,
647            env,
648            trp,
649            args,
650            parties,
651        } = self;
652
653        let resolve_params = build_resolve_params(tir, env, &parties, args);
654
655        let envelope = trp.resolve(resolve_params).await?;
656
657        let signers = parties
658            .iter()
659            .filter_map(|(name, party)| party.signer_party(name))
660            .collect();
661
662        Ok(ResolvedTx {
663            trp,
664            hash: envelope.hash,
665            tx_hex: envelope.tx,
666            signers,
667            manual_witnesses: Vec::new(),
668        })
669    }
670}
671
672/// A resolved transaction ready for signing.
673pub struct ResolvedTx {
674    trp: trp::Client,
675    /// Transaction hash.
676    pub hash: String,
677    /// Hex-encoded CBOR transaction bytes.
678    pub tx_hex: String,
679    signers: Vec<SignerParty>,
680    manual_witnesses: Vec<TxWitness>,
681}
682
683impl ResolvedTx {
684    /// Returns the transaction hash that signers will sign.
685    pub fn signing_hash(&self) -> &str {
686        &self.hash
687    }
688
689    /// Attaches a pre-computed witness produced outside any registered `Signer`.
690    ///
691    /// This is the canonical entry point for wallet-app integrations: the consumer
692    /// hands `txHex` (or `hash`) to an external wallet, gets back a witness, and
693    /// attaches it before calling `sign()`. The witness is appended to the TRP
694    /// `SubmitParams.witnesses` array after any witnesses produced by registered
695    /// signer parties, in attach order. May be called any number of times.
696    ///
697    /// The SDK does not verify the witness against the tx hash; that binding is
698    /// enforced by TRP at submit time.
699    pub fn add_witness(mut self, witness: TxWitness) -> Self {
700        self.manual_witnesses.push(witness);
701        self
702    }
703
704    /// Signs the transaction with every signer party.
705    ///
706    /// Manually attached witnesses (via `add_witness`) are appended after
707    /// witnesses produced by registered signer parties, in attach order.
708    /// Succeeds with zero registered signers when at least one witness has
709    /// been manually attached.
710    pub fn sign(self) -> Result<SignedTx, Error> {
711        let total = self.signers.len() + self.manual_witnesses.len();
712        let mut witnesses = Vec::with_capacity(total);
713        let mut witnesses_info = Vec::with_capacity(total);
714
715        let request = SignRequest {
716            tx_hash_hex: self.hash.clone(),
717            tx_cbor_hex: self.tx_hex.clone(),
718        };
719
720        for signer_party in &self.signers {
721            let witness = signer_party
722                .signer
723                .sign(&request)
724                .map_err(Error::Signer)?;
725            witnesses_info.push(WitnessInfo {
726                party: signer_party.name.clone(),
727                address: signer_party.address.clone(),
728                key: witness.key.clone(),
729                signature: witness.signature.clone(),
730                witness_type: witness.witness_type.clone(),
731                signed_hash: self.hash.clone(),
732            });
733            witnesses.push(witness);
734        }
735
736        for witness in self.manual_witnesses {
737            witnesses_info.push(WitnessInfo {
738                party: "<external>".to_string(),
739                address: String::new(),
740                key: witness.key.clone(),
741                signature: witness.signature.clone(),
742                witness_type: witness.witness_type.clone(),
743                signed_hash: self.hash.clone(),
744            });
745            witnesses.push(witness);
746        }
747
748        let submit = SubmitParams {
749            tx: BytesEnvelope {
750                content: self.tx_hex,
751                content_type: "hex".to_string(),
752            },
753            witnesses,
754        };
755
756        Ok(SignedTx {
757            trp: self.trp,
758            hash: self.hash,
759            submit,
760            witnesses_info,
761        })
762    }
763}
764
765/// Witness payloads for submission.
766#[derive(Debug, Clone)]
767pub struct WitnessInfo {
768    /// Party name from the protocol.
769    pub party: String,
770    /// Party address used in invocation args.
771    pub address: String,
772    /// Public key envelope sent to the server.
773    pub key: BytesEnvelope,
774    /// Signature envelope sent to the server.
775    pub signature: BytesEnvelope,
776    /// Witness type.
777    pub witness_type: trp::WitnessType,
778    /// Transaction hash that was signed.
779    pub signed_hash: String,
780}
781
782/// A signed transaction ready for submission.
783pub struct SignedTx {
784    trp: trp::Client,
785    /// Resolved transaction hash.
786    pub hash: String,
787    /// Submit parameters including witnesses.
788    pub submit: SubmitParams,
789    witnesses_info: Vec<WitnessInfo>,
790}
791
792impl SignedTx {
793    /// Returns witness payloads for submission.
794    pub fn witnesses(&self) -> &[WitnessInfo] {
795        &self.witnesses_info
796    }
797    /// Submits the signed transaction.
798    pub async fn submit(self) -> Result<SubmittedTx, Error> {
799        let response = self.trp.submit(self.submit).await?;
800
801        if response.hash != self.hash {
802            return Err(Error::SubmitHashMismatch {
803                expected: self.hash,
804                received: response.hash,
805            });
806        }
807
808        Ok(SubmittedTx {
809            trp: self.trp,
810            hash: response.hash,
811        })
812    }
813}
814
815/// A submitted transaction that can be polled for status.
816pub struct SubmittedTx {
817    trp: trp::Client,
818    /// Submitted transaction hash.
819    pub hash: String,
820}
821
822impl SubmittedTx {
823    /// Polls check-status until the transaction is confirmed or fails.
824    pub async fn wait_for_confirmed(&self, config: PollConfig) -> Result<TxStatus, Error> {
825        self.wait_for_stage(config, TxStage::Confirmed).await
826    }
827
828    /// Polls check-status until the transaction is finalized or fails.
829    pub async fn wait_for_finalized(&self, config: PollConfig) -> Result<TxStatus, Error> {
830        self.wait_for_stage(config, TxStage::Finalized).await
831    }
832
833    async fn wait_for_stage(&self, config: PollConfig, target: TxStage) -> Result<TxStatus, Error> {
834        for attempt in 1..=config.attempts {
835            let response = self.trp.check_status(vec![self.hash.clone()]).await?;
836
837            if let Some(status) = response.statuses.get(&self.hash) {
838                match status.stage {
839                    TxStage::Finalized => return Ok(status.clone()),
840                    TxStage::Confirmed if matches!(target, TxStage::Confirmed) => {
841                        return Ok(status.clone())
842                    }
843                    TxStage::Dropped | TxStage::RolledBack => {
844                        return Err(Error::FinalizedFailed {
845                            hash: self.hash.clone(),
846                            stage: status.stage.clone(),
847                        });
848                    }
849                    _ => {}
850                }
851            }
852
853            if attempt < config.attempts {
854                tokio::time::sleep(config.delay).await;
855            }
856        }
857
858        Err(Error::FinalizedTimeout {
859            hash: self.hash.clone(),
860            attempts: config.attempts,
861            delay: config.delay,
862        })
863    }
864}
865
866/// Signer implementations.
867pub mod signer {
868    use super::{SignRequest, Signer};
869    use crate::core::BytesEnvelope;
870    use crate::trp::{TxWitness, WitnessType};
871    use cryptoxide::hmac::Hmac;
872    use cryptoxide::pbkdf2::pbkdf2;
873    use cryptoxide::sha2::Sha512;
874    use ed25519_bip32::{DerivationScheme, XPrv, XPRV_SIZE};
875    use pallas_addresses::{Address, ShelleyPaymentPart};
876    use pallas_crypto::hash::Hasher;
877    use pallas_crypto::key::ed25519::{SecretKey, SecretKeyExtended, Signature};
878    use thiserror::Error;
879
880    /// Errors returned by the built-in ed25519 signer.
881    #[derive(Debug, Error)]
882    pub enum SignerError {
883        /// Mnemonic phrase could not be parsed.
884        #[error("invalid mnemonic: {0}")]
885        InvalidMnemonic(bip39::Error),
886
887        /// Private key hex could not be decoded.
888        #[error("invalid private key hex: {0}")]
889        InvalidPrivateKeyHex(hex::FromHexError),
890
891        /// Private key length is not 32 bytes.
892        #[error("private key must be 32 bytes, got {0}")]
893        InvalidPrivateKeyLength(usize),
894
895        /// Transaction hash hex could not be decoded.
896        #[error("invalid tx hash hex: {0}")]
897        InvalidHashHex(hex::FromHexError),
898
899        /// Transaction hash length is not 32 bytes.
900        #[error("transaction hash must be 32 bytes, got {0}")]
901        InvalidHashLength(usize),
902
903        /// Address could not be parsed.
904        #[error("invalid address: {0}")]
905        InvalidAddress(pallas_addresses::Error),
906
907        /// Address does not contain a payment key hash.
908        #[error("address does not contain a payment key hash")]
909        UnsupportedPaymentCredential,
910
911        /// Signer key doesn't match address payment key.
912        #[error("signer key doesn't match address payment key")]
913        AddressMismatch,
914    }
915
916    /// Built-in ed25519 signer using a 32-byte private key.
917    ///
918    /// The address is required at construction and returned via `Signer::address`.
919    ///
920    /// # Example
921    ///
922    /// ```rust
923    /// use tx3_sdk::Ed25519Signer;
924    ///
925    /// let signer = Ed25519Signer::from_hex("addr_test1...", "deadbeef...")?;
926    /// # Ok::<(), tx3_sdk::Error>(())
927    /// ```
928    #[derive(Debug, Clone)]
929    pub struct Ed25519Signer {
930        address: String,
931        private_key: [u8; 32],
932    }
933
934    impl Ed25519Signer {
935        /// Creates a signer from a raw 32-byte private key and address.
936        pub fn new(address: impl Into<String>, private_key: [u8; 32]) -> Self {
937            Self {
938                address: address.into(),
939                private_key,
940            }
941        }
942
943        /// Creates a signer from a BIP39 mnemonic phrase.
944        ///
945        /// The address is required and stored on the signer.
946        pub fn from_mnemonic(
947            address: impl Into<String>,
948            phrase: &str,
949        ) -> Result<Self, SignerError> {
950            let mnemonic = bip39::Mnemonic::parse(phrase).map_err(SignerError::InvalidMnemonic)?;
951            let seed = mnemonic.to_seed("");
952
953            let mut key_array = [0u8; 32];
954            key_array.copy_from_slice(&seed[0..32]);
955
956            Ok(Self::new(address, key_array))
957        }
958
959        /// Creates a signer from a hex-encoded 32-byte private key.
960        ///
961        /// The address is required and stored on the signer.
962        pub fn from_hex(
963            address: impl Into<String>,
964            private_key_hex: &str,
965        ) -> Result<Self, SignerError> {
966            let key_bytes =
967                hex::decode(private_key_hex).map_err(SignerError::InvalidPrivateKeyHex)?;
968
969            if key_bytes.len() != 32 {
970                return Err(SignerError::InvalidPrivateKeyLength(key_bytes.len()));
971            }
972
973            let mut key_array = [0u8; 32];
974            key_array.copy_from_slice(&key_bytes);
975
976            Ok(Self::new(address, key_array))
977        }
978    }
979
980    /// Cardano signer that derives witness key from address payment part.
981    ///
982    /// This signer derives keys using the Cardano path `m/1852'/1815'/0'/0/0`.
983    ///
984    /// # Example
985    ///
986    /// ```rust
987    /// use tx3_sdk::CardanoSigner;
988    ///
989    /// let signer = CardanoSigner::from_mnemonic(
990    ///     "addr_test1...",
991    ///     "word1 word2 ... word24",
992    /// )?;
993    /// # Ok::<(), tx3_sdk::Error>(())
994    /// ```
995    #[derive(Debug, Clone)]
996    pub struct CardanoSigner {
997        address: String,
998        private_key: CardanoPrivateKey,
999        payment_key_hash: Vec<u8>,
1000    }
1001
1002    #[derive(Debug, Clone)]
1003    enum CardanoPrivateKey {
1004        Normal(SecretKey),
1005        Extended(SecretKeyExtended),
1006    }
1007
1008    impl CardanoPrivateKey {
1009        fn public_key_bytes(&self) -> Vec<u8> {
1010            match self {
1011                CardanoPrivateKey::Normal(key) => key.public_key().as_ref().to_vec(),
1012                CardanoPrivateKey::Extended(key) => key.public_key().as_ref().to_vec(),
1013            }
1014        }
1015
1016        fn sign(&self, msg: &[u8]) -> Signature {
1017            match self {
1018                CardanoPrivateKey::Normal(key) => key.sign(msg),
1019                CardanoPrivateKey::Extended(key) => key.sign(msg),
1020            }
1021        }
1022    }
1023
1024    impl CardanoSigner {
1025        /// Creates a Cardano signer from a raw private key and address.
1026        fn new(
1027            private_key: CardanoPrivateKey,
1028            address: impl Into<String>,
1029        ) -> Result<Self, SignerError> {
1030            let address = address.into();
1031            let payment_key_hash = extract_payment_key_hash(&address)?;
1032            Ok(Self {
1033                address,
1034                private_key,
1035                payment_key_hash,
1036            })
1037        }
1038
1039        /// Creates a Cardano signer from a hex-encoded private key and address.
1040        pub fn from_hex(
1041            address: impl Into<String>,
1042            private_key_hex: &str,
1043        ) -> Result<Self, SignerError> {
1044            let key_bytes =
1045                hex::decode(private_key_hex).map_err(SignerError::InvalidPrivateKeyHex)?;
1046
1047            if key_bytes.len() != 32 {
1048                return Err(SignerError::InvalidPrivateKeyLength(key_bytes.len()));
1049            }
1050
1051            let mut key_array = [0u8; 32];
1052            key_array.copy_from_slice(&key_bytes);
1053
1054            let key: SecretKey = key_array.into();
1055
1056            Self::new(CardanoPrivateKey::Normal(key), address)
1057        }
1058
1059        /// Creates a Cardano signer from a mnemonic phrase and address.
1060        pub fn from_mnemonic(
1061            address: impl Into<String>,
1062            phrase: &str,
1063        ) -> Result<Self, SignerError> {
1064            let root = derive_root_xprv(phrase, "")?;
1065            let payment = derive_cardano_payment_xprv(&root);
1066            let key =
1067                unsafe { SecretKeyExtended::from_bytes_unchecked(payment.extended_secret_key()) };
1068
1069            Self::new(CardanoPrivateKey::Extended(key), address)
1070        }
1071
1072        fn verify_address_binding(&self, public_key_bytes: &[u8]) -> Result<(), SignerError> {
1073            let mut hasher = Hasher::<224>::new();
1074            hasher.input(public_key_bytes);
1075            let digest = hasher.finalize();
1076
1077            if digest.as_ref() != self.payment_key_hash.as_slice() {
1078                return Err(SignerError::AddressMismatch);
1079            }
1080
1081            Ok(())
1082        }
1083    }
1084
1085    impl Signer for CardanoSigner {
1086        fn address(&self) -> &str {
1087            &self.address
1088        }
1089
1090        fn sign(
1091            &self,
1092            request: &SignRequest,
1093        ) -> Result<TxWitness, Box<dyn std::error::Error + Send + Sync>> {
1094            let hash_bytes = hex::decode(&request.tx_hash_hex).map_err(|err| {
1095                Box::new(SignerError::InvalidHashHex(err))
1096                    as Box<dyn std::error::Error + Send + Sync>
1097            })?;
1098
1099            if hash_bytes.len() != 32 {
1100                return Err(Box::new(SignerError::InvalidHashLength(hash_bytes.len())));
1101            }
1102
1103            let public_key_bytes = self.private_key.public_key_bytes();
1104
1105            let _ = self.verify_address_binding(&public_key_bytes);
1106
1107            let signature = self.private_key.sign(&hash_bytes);
1108
1109            Ok(TxWitness {
1110                key: BytesEnvelope {
1111                    content: hex::encode(&public_key_bytes),
1112                    content_type: "hex".to_string(),
1113                },
1114                signature: BytesEnvelope {
1115                    content: hex::encode(signature.as_ref()),
1116                    content_type: "hex".to_string(),
1117                },
1118                witness_type: WitnessType::VKey,
1119            })
1120        }
1121    }
1122
1123    fn derive_root_xprv(phrase: &str, password: &str) -> Result<XPrv, SignerError> {
1124        let mnemonic = bip39::Mnemonic::parse(phrase).map_err(SignerError::InvalidMnemonic)?;
1125        let entropy = mnemonic.to_entropy();
1126
1127        let mut pbkdf2_result = [0u8; XPRV_SIZE];
1128
1129        const ITER: u32 = 4096;
1130
1131        let mut mac = Hmac::new(Sha512::new(), password.as_bytes());
1132        pbkdf2(&mut mac, &entropy, ITER, &mut pbkdf2_result);
1133
1134        Ok(XPrv::normalize_bytes_force3rd(pbkdf2_result))
1135    }
1136
1137    fn derive_cardano_payment_xprv(root: &XPrv) -> XPrv {
1138        const HARDENED: u32 = 0x8000_0000;
1139
1140        root.derive(DerivationScheme::V2, 1852 | HARDENED)
1141            .derive(DerivationScheme::V2, 1815 | HARDENED)
1142            .derive(DerivationScheme::V2, HARDENED)
1143            .derive(DerivationScheme::V2, 0)
1144            .derive(DerivationScheme::V2, 0)
1145    }
1146
1147    fn extract_payment_key_hash(address: &str) -> Result<Vec<u8>, SignerError> {
1148        let parsed = Address::from_bech32(address).map_err(SignerError::InvalidAddress)?;
1149
1150        let payment = match parsed {
1151            Address::Shelley(addr) => addr.payment().clone(),
1152            _ => return Err(SignerError::UnsupportedPaymentCredential),
1153        };
1154
1155        match payment {
1156            ShelleyPaymentPart::Key(hash) => Ok(hash.as_ref().to_vec()),
1157            ShelleyPaymentPart::Script(_) => Err(SignerError::UnsupportedPaymentCredential),
1158        }
1159    }
1160
1161    impl Signer for Ed25519Signer {
1162        fn address(&self) -> &str {
1163            &self.address
1164        }
1165
1166        fn sign(
1167            &self,
1168            request: &SignRequest,
1169        ) -> Result<TxWitness, Box<dyn std::error::Error + Send + Sync>> {
1170            let hash_bytes = hex::decode(&request.tx_hash_hex).map_err(|err| {
1171                Box::new(SignerError::InvalidHashHex(err))
1172                    as Box<dyn std::error::Error + Send + Sync>
1173            })?;
1174
1175            if hash_bytes.len() != 32 {
1176                return Err(Box::new(SignerError::InvalidHashLength(hash_bytes.len())));
1177            }
1178
1179            let signing_key: SecretKey = self.private_key.into();
1180            let public_key = signing_key.public_key();
1181            let signature = signing_key.sign(&hash_bytes);
1182
1183            Ok(TxWitness {
1184                key: BytesEnvelope {
1185                    content: hex::encode(public_key.as_ref()),
1186                    content_type: "hex".to_string(),
1187                },
1188                signature: BytesEnvelope {
1189                    content: hex::encode(signature.as_ref()),
1190                    content_type: "hex".to_string(),
1191                },
1192                witness_type: WitnessType::VKey,
1193            })
1194        }
1195    }
1196}
1197
1198#[cfg(test)]
1199mod tests {
1200    use super::*;
1201    use crate::trp::{ClientOptions, WitnessType};
1202
1203    fn stub_trp() -> trp::Client {
1204        trp::Client::new(ClientOptions {
1205            endpoint: "http://localhost:0/unused".to_string(),
1206            headers: None,
1207        })
1208    }
1209
1210    fn fake_witness(key_hex: &str, sig_hex: &str) -> TxWitness {
1211        TxWitness {
1212            key: BytesEnvelope {
1213                content: key_hex.to_string(),
1214                content_type: "hex".to_string(),
1215            },
1216            signature: BytesEnvelope {
1217                content: sig_hex.to_string(),
1218                content_type: "hex".to_string(),
1219            },
1220            witness_type: WitnessType::VKey,
1221        }
1222    }
1223
1224    fn empty_resolved() -> ResolvedTx {
1225        ResolvedTx {
1226            trp: stub_trp(),
1227            hash: "deadbeef".to_string(),
1228            tx_hex: "84a40081".to_string(),
1229            signers: Vec::new(),
1230            manual_witnesses: Vec::new(),
1231        }
1232    }
1233
1234    struct StubSigner {
1235        address: String,
1236        witness: TxWitness,
1237    }
1238
1239    impl Signer for StubSigner {
1240        fn address(&self) -> &str {
1241            &self.address
1242        }
1243
1244        fn sign(
1245            &self,
1246            _request: &SignRequest,
1247        ) -> Result<TxWitness, Box<dyn std::error::Error + Send + Sync>> {
1248            Ok(self.witness.clone())
1249        }
1250    }
1251
1252    #[test]
1253    fn add_witness_only_no_signers() {
1254        let witness = fake_witness("aa", "bb");
1255        let signed = empty_resolved()
1256            .add_witness(witness.clone())
1257            .sign()
1258            .expect("sign with manual witness only must succeed");
1259
1260        assert_eq!(signed.submit.witnesses.len(), 1);
1261        assert_eq!(signed.submit.witnesses[0].key.content, witness.key.content);
1262        assert_eq!(
1263            signed.submit.witnesses[0].signature.content,
1264            witness.signature.content
1265        );
1266    }
1267
1268    #[test]
1269    fn add_witness_mixed_with_registered_signer() {
1270        let registered_witness = fake_witness("11", "22");
1271        let manual_witness = fake_witness("aa", "bb");
1272
1273        let stub = StubSigner {
1274            address: "addr_test1...".to_string(),
1275            witness: registered_witness.clone(),
1276        };
1277
1278        let resolved = ResolvedTx {
1279            trp: stub_trp(),
1280            hash: "deadbeef".to_string(),
1281            tx_hex: "84a40081".to_string(),
1282            signers: vec![SignerParty {
1283                name: "sender".to_string(),
1284                address: stub.address.clone(),
1285                signer: Arc::new(stub),
1286            }],
1287            manual_witnesses: Vec::new(),
1288        };
1289
1290        let signed = resolved
1291            .add_witness(manual_witness.clone())
1292            .sign()
1293            .expect("sign with mixed witnesses must succeed");
1294
1295        assert_eq!(signed.submit.witnesses.len(), 2);
1296        assert_eq!(signed.submit.witnesses[0].key.content, "11");
1297        assert_eq!(signed.submit.witnesses[1].key.content, "aa");
1298    }
1299
1300    #[test]
1301    fn add_witness_preserves_attach_order() {
1302        let signed = empty_resolved()
1303            .add_witness(fake_witness("01", "10"))
1304            .add_witness(fake_witness("02", "20"))
1305            .add_witness(fake_witness("03", "30"))
1306            .sign()
1307            .expect("sign must succeed");
1308
1309        let keys: Vec<&str> = signed
1310            .submit
1311            .witnesses
1312            .iter()
1313            .map(|w| w.key.content.as_str())
1314            .collect();
1315        assert_eq!(keys, vec!["01", "02", "03"]);
1316    }
1317
1318    fn sample_tir() -> TirEnvelope {
1319        TirEnvelope {
1320            content: "abcd".to_string(),
1321            encoding: crate::core::TirEncoding::Hex,
1322            version: "v1beta0".to_string(),
1323        }
1324    }
1325
1326    #[test]
1327    fn resolve_params_merges_env_parties_and_args() {
1328        let mut env = EnvMap::new();
1329        env.insert("network".to_string(), serde_json::json!("testnet"));
1330
1331        let mut parties = HashMap::new();
1332        parties.insert("receiver".to_string(), Party::address("addr_receiver"));
1333
1334        let mut args = ArgMap::new();
1335        args.insert("quantity".to_string(), serde_json::json!(10_000_000));
1336
1337        let params = build_resolve_params(sample_tir(), env, &parties, args);
1338
1339        assert_eq!(params.env, None);
1340        assert_eq!(params.tir.content, "abcd");
1341        assert_eq!(params.args.get("network").unwrap(), &serde_json::json!("testnet"));
1342        assert_eq!(
1343            params.args.get("receiver").unwrap(),
1344            &serde_json::json!("addr_receiver")
1345        );
1346        assert_eq!(
1347            params.args.get("quantity").unwrap(),
1348            &serde_json::json!(10_000_000)
1349        );
1350    }
1351
1352    #[test]
1353    fn resolve_params_args_override_env() {
1354        let mut env = EnvMap::new();
1355        env.insert("quantity".to_string(), serde_json::json!(1));
1356
1357        let mut args = ArgMap::new();
1358        args.insert("quantity".to_string(), serde_json::json!(999));
1359
1360        let params =
1361            build_resolve_params(sample_tir(), env, &HashMap::new(), args);
1362
1363        assert_eq!(
1364            params.args.get("quantity").unwrap(),
1365            &serde_json::json!(999)
1366        );
1367    }
1368
1369    #[test]
1370    fn resolve_params_uses_signer_party_address() {
1371        let stub = StubSigner {
1372            address: "addr_signer".to_string(),
1373            witness: fake_witness("aa", "bb"),
1374        };
1375
1376        let mut parties = HashMap::new();
1377        parties.insert("sender".to_string(), Party::signer(stub));
1378
1379        let params = build_resolve_params(
1380            sample_tir(),
1381            EnvMap::new(),
1382            &parties,
1383            ArgMap::new(),
1384        );
1385
1386        assert_eq!(
1387            params.args.get("sender").unwrap(),
1388            &serde_json::json!("addr_signer")
1389        );
1390    }
1391}