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_json::Value;
11use thiserror::Error;
12
13use crate::core::{ArgMap, BytesEnvelope};
14use crate::tii::Protocol;
15use crate::trp::{self, SubmitParams, TxStage, TxStatus, TxWitness};
16
17#[derive(Clone)]
18struct SignerParty {
19    name: String,
20    address: String,
21    signer: Arc<dyn Signer + Send + Sync>,
22}
23
24/// Error type for facade operations.
25#[derive(Debug, Error)]
26pub enum Error {
27    /// Error originating from TII operations.
28    #[error(transparent)]
29    Tii(#[from] crate::tii::Error),
30
31    /// Error originating from TRP operations.
32    #[error(transparent)]
33    Trp(#[from] crate::trp::Error),
34
35    /// Required parameters were not provided.
36    #[error("missing required params: {0:?}")]
37    MissingParams(Vec<String>),
38
39    /// A party was provided but not declared in the protocol.
40    #[error("unknown party: {0}")]
41    UnknownParty(String),
42
43    /// Signer failed to produce a witness.
44    #[error("signer error: {0}")]
45    Signer(#[source] Box<dyn std::error::Error + Send + Sync>),
46
47    /// Submitted hash does not match the resolved hash.
48    #[error("submit hash mismatch: expected {expected}, got {received}")]
49    SubmitHashMismatch { expected: String, received: String },
50
51    /// Transaction failed to reach confirmation.
52    #[error("tx {hash} failed with stage {stage:?}")]
53    FinalizedFailed { hash: String, stage: TxStage },
54
55    /// Transaction did not reach confirmation within the polling window.
56    #[error("tx {hash} not confirmed after {attempts} attempts (delay {delay:?})")]
57    FinalizedTimeout {
58        hash: String,
59        attempts: u32,
60        delay: Duration,
61    },
62}
63
64/// Configuration for check-status polling.
65///
66/// Used by `wait_for_confirmed` and `wait_for_finalized`.
67#[derive(Debug, Clone)]
68pub struct PollConfig {
69    /// Number of attempts before timing out.
70    pub attempts: u32,
71    /// Delay between attempts.
72    pub delay: Duration,
73}
74
75impl Default for PollConfig {
76    fn default() -> Self {
77        Self {
78            attempts: 20,
79            delay: Duration::from_secs(5),
80        }
81    }
82}
83
84/// Inputs passed to a [`Signer`] for each sign call.
85///
86/// Carries both the bound tx hash and the full hex-encoded tx CBOR. Hash-based
87/// signers (Cardano, Ed25519) read `tx_hash_hex`; tx-based signers (e.g. wallet
88/// adapters that need the full tx body) read `tx_cbor_hex`. The SDK always
89/// populates both fields.
90#[derive(Debug, Clone)]
91pub struct SignRequest {
92    /// Hex-encoded tx hash bound to this signing call.
93    pub tx_hash_hex: String,
94    /// Hex-encoded full tx CBOR.
95    pub tx_cbor_hex: String,
96}
97
98/// A signer capable of producing TRP witnesses.
99///
100/// Signers are address-aware and must return the address they correspond to.
101pub trait Signer: Send + Sync {
102    /// Returns the address associated with this signer.
103    fn address(&self) -> &str;
104
105    /// Signs the transaction described by `request`.
106    fn sign(
107        &self,
108        request: &SignRequest,
109    ) -> Result<TxWitness, Box<dyn std::error::Error + Send + Sync>>;
110}
111
112/// A party referenced by the protocol.
113#[derive(Clone)]
114pub enum Party {
115    /// Read-only party with a known address.
116    Address(String),
117    /// Party capable of signing transactions.
118    Signer {
119        /// Party address (used for invocation args).
120        address: String,
121        /// Signer implementation.
122        signer: Arc<dyn Signer + Send + Sync>,
123    },
124}
125
126impl Party {
127    /// Creates a read-only party from an address.
128    pub fn address(address: impl Into<String>) -> Self {
129        Party::Address(address.into())
130    }
131
132    /// Creates a signer party from a signer.
133    ///
134    /// The party address is taken from the signer itself.
135    ///
136    /// # Example
137    ///
138    /// ```rust
139    /// use tx3_sdk::{CardanoSigner, Party};
140    ///
141    /// let signer = CardanoSigner::from_hex("addr_test1...", "deadbeef...")?;
142    /// let party = Party::signer(signer);
143    /// # Ok::<(), tx3_sdk::Error>(())
144    /// ```
145    pub fn signer(signer: impl Signer + 'static) -> Self {
146        Party::Signer {
147            address: signer.address().to_string(),
148            signer: Arc::new(signer),
149        }
150    }
151
152    fn address_value(&self) -> &str {
153        match self {
154            Party::Address(address) => address,
155            Party::Signer { address, .. } => address,
156        }
157    }
158
159    fn signer_party(&self, name: &str) -> Option<SignerParty> {
160        match self {
161            Party::Signer { address, signer } => Some(SignerParty {
162                name: name.to_string(),
163                address: address.clone(),
164                signer: Arc::clone(signer),
165            }),
166            _ => None,
167        }
168    }
169}
170
171/// High-level client that ties a protocol to a TRP client.
172#[derive(Clone)]
173pub struct Tx3Client {
174    protocol: Arc<Protocol>,
175    trp: trp::Client,
176    parties: HashMap<String, Party>,
177    profile: Option<String>,
178}
179
180impl Tx3Client {
181    /// Creates a new facade client.
182    pub fn new(protocol: Protocol, trp: trp::Client) -> Self {
183        Self {
184            protocol: Arc::new(protocol),
185            trp,
186            parties: HashMap::new(),
187            profile: None,
188        }
189    }
190
191    /// Sets the profile for all invocations created by this client.
192    ///
193    /// This profile is applied to every invocation created by the client.
194    pub fn with_profile(mut self, profile: impl Into<String>) -> Self {
195        self.profile = Some(profile.into());
196        self
197    }
198
199    /// Attaches a party definition to this client.
200    pub fn with_party(mut self, name: impl Into<String>, party: Party) -> Self {
201        self.parties.insert(name.into().to_lowercase(), party);
202        self
203    }
204
205    /// Attaches multiple party definitions to this client.
206    pub fn with_parties<I, K>(mut self, parties: I) -> Self
207    where
208        I: IntoIterator<Item = (K, Party)>,
209        K: Into<String>,
210    {
211        for (name, party) in parties {
212            self.parties.insert(name.into().to_lowercase(), party);
213        }
214        self
215    }
216
217    /// Starts building a transaction invocation.
218    pub fn tx(&self, name: impl Into<String>) -> TxBuilder {
219        TxBuilder {
220            protocol: Arc::clone(&self.protocol),
221            trp: self.trp.clone(),
222            tx_name: name.into(),
223            args: ArgMap::new(),
224            parties: self.parties.clone(),
225            profile: self.profile.clone(),
226        }
227    }
228}
229
230/// Builder for transaction invocation.
231pub struct TxBuilder {
232    protocol: Arc<Protocol>,
233    trp: trp::Client,
234    tx_name: String,
235    args: ArgMap,
236    parties: HashMap<String, Party>,
237    profile: Option<String>,
238}
239
240impl TxBuilder {
241    /// Adds a single argument (case-insensitive name).
242    pub fn arg(mut self, name: &str, value: impl Into<Value>) -> Self {
243        self.args.insert(name.to_lowercase(), value.into());
244        self
245    }
246
247    /// Adds multiple arguments (case-insensitive names).
248    pub fn args(mut self, args: ArgMap) -> Self {
249        for (key, value) in args {
250            self.args.insert(key.to_lowercase(), value);
251        }
252        self
253    }
254
255    /// Resolves the transaction using the TRP client.
256    pub async fn resolve(self) -> Result<ResolvedTx, Error> {
257        let mut invocation = self
258            .protocol
259            .invoke(&self.tx_name, self.profile.as_deref())?;
260
261        let known_parties: HashSet<String> = self
262            .protocol
263            .parties()
264            .keys()
265            .map(|key| key.to_lowercase())
266            .collect();
267
268        for (name, party) in &self.parties {
269            if !known_parties.contains(name) {
270                return Err(Error::UnknownParty(name.clone()));
271            }
272
273            invocation.set_arg(
274                name,
275                serde_json::Value::String(party.address_value().to_string()),
276            );
277        }
278
279        invocation.set_args(self.args);
280
281        let mut missing: Vec<String> = invocation
282            .unspecified_params()
283            .map(|(key, _)| key.clone())
284            .collect();
285
286        if !missing.is_empty() {
287            missing.sort();
288            return Err(Error::MissingParams(missing));
289        }
290
291        let resolve_params = invocation.into_resolve_request()?;
292        let envelope = self.trp.resolve(resolve_params).await?;
293
294        let signers = self
295            .parties
296            .iter()
297            .filter_map(|(name, party)| party.signer_party(name))
298            .collect();
299
300        Ok(ResolvedTx {
301            trp: self.trp,
302            hash: envelope.hash,
303            tx_hex: envelope.tx,
304            signers,
305            manual_witnesses: Vec::new(),
306        })
307    }
308}
309
310/// A resolved transaction ready for signing.
311pub struct ResolvedTx {
312    trp: trp::Client,
313    /// Transaction hash.
314    pub hash: String,
315    /// Hex-encoded CBOR transaction bytes.
316    pub tx_hex: String,
317    signers: Vec<SignerParty>,
318    manual_witnesses: Vec<TxWitness>,
319}
320
321impl ResolvedTx {
322    /// Returns the transaction hash that signers will sign.
323    pub fn signing_hash(&self) -> &str {
324        &self.hash
325    }
326
327    /// Attaches a pre-computed witness produced outside any registered `Signer`.
328    ///
329    /// This is the canonical entry point for wallet-app integrations: the consumer
330    /// hands `txHex` (or `hash`) to an external wallet, gets back a witness, and
331    /// attaches it before calling `sign()`. The witness is appended to the TRP
332    /// `SubmitParams.witnesses` array after any witnesses produced by registered
333    /// signer parties, in attach order. May be called any number of times.
334    ///
335    /// The SDK does not verify the witness against the tx hash; that binding is
336    /// enforced by TRP at submit time.
337    pub fn add_witness(mut self, witness: TxWitness) -> Self {
338        self.manual_witnesses.push(witness);
339        self
340    }
341
342    /// Signs the transaction with every signer party.
343    ///
344    /// Manually attached witnesses (via `add_witness`) are appended after
345    /// witnesses produced by registered signer parties, in attach order.
346    /// Succeeds with zero registered signers when at least one witness has
347    /// been manually attached.
348    pub fn sign(self) -> Result<SignedTx, Error> {
349        let total = self.signers.len() + self.manual_witnesses.len();
350        let mut witnesses = Vec::with_capacity(total);
351        let mut witnesses_info = Vec::with_capacity(total);
352
353        let request = SignRequest {
354            tx_hash_hex: self.hash.clone(),
355            tx_cbor_hex: self.tx_hex.clone(),
356        };
357
358        for signer_party in &self.signers {
359            let witness = signer_party
360                .signer
361                .sign(&request)
362                .map_err(Error::Signer)?;
363            witnesses_info.push(WitnessInfo {
364                party: signer_party.name.clone(),
365                address: signer_party.address.clone(),
366                key: witness.key.clone(),
367                signature: witness.signature.clone(),
368                witness_type: witness.witness_type.clone(),
369                signed_hash: self.hash.clone(),
370            });
371            witnesses.push(witness);
372        }
373
374        for witness in self.manual_witnesses {
375            witnesses_info.push(WitnessInfo {
376                party: "<external>".to_string(),
377                address: String::new(),
378                key: witness.key.clone(),
379                signature: witness.signature.clone(),
380                witness_type: witness.witness_type.clone(),
381                signed_hash: self.hash.clone(),
382            });
383            witnesses.push(witness);
384        }
385
386        let submit = SubmitParams {
387            tx: BytesEnvelope {
388                content: self.tx_hex,
389                content_type: "hex".to_string(),
390            },
391            witnesses,
392        };
393
394        Ok(SignedTx {
395            trp: self.trp,
396            hash: self.hash,
397            submit,
398            witnesses_info,
399        })
400    }
401}
402
403/// Witness payloads for submission.
404#[derive(Debug, Clone)]
405pub struct WitnessInfo {
406    /// Party name from the protocol.
407    pub party: String,
408    /// Party address used in invocation args.
409    pub address: String,
410    /// Public key envelope sent to the server.
411    pub key: BytesEnvelope,
412    /// Signature envelope sent to the server.
413    pub signature: BytesEnvelope,
414    /// Witness type.
415    pub witness_type: trp::WitnessType,
416    /// Transaction hash that was signed.
417    pub signed_hash: String,
418}
419
420/// A signed transaction ready for submission.
421pub struct SignedTx {
422    trp: trp::Client,
423    /// Resolved transaction hash.
424    pub hash: String,
425    /// Submit parameters including witnesses.
426    pub submit: SubmitParams,
427    witnesses_info: Vec<WitnessInfo>,
428}
429
430impl SignedTx {
431    /// Returns witness payloads for submission.
432    pub fn witnesses(&self) -> &[WitnessInfo] {
433        &self.witnesses_info
434    }
435    /// Submits the signed transaction.
436    pub async fn submit(self) -> Result<SubmittedTx, Error> {
437        let response = self.trp.submit(self.submit).await?;
438
439        if response.hash != self.hash {
440            return Err(Error::SubmitHashMismatch {
441                expected: self.hash,
442                received: response.hash,
443            });
444        }
445
446        Ok(SubmittedTx {
447            trp: self.trp,
448            hash: response.hash,
449        })
450    }
451}
452
453/// A submitted transaction that can be polled for status.
454pub struct SubmittedTx {
455    trp: trp::Client,
456    /// Submitted transaction hash.
457    pub hash: String,
458}
459
460impl SubmittedTx {
461    /// Polls check-status until the transaction is confirmed or fails.
462    pub async fn wait_for_confirmed(&self, config: PollConfig) -> Result<TxStatus, Error> {
463        self.wait_for_stage(config, TxStage::Confirmed).await
464    }
465
466    /// Polls check-status until the transaction is finalized or fails.
467    pub async fn wait_for_finalized(&self, config: PollConfig) -> Result<TxStatus, Error> {
468        self.wait_for_stage(config, TxStage::Finalized).await
469    }
470
471    async fn wait_for_stage(&self, config: PollConfig, target: TxStage) -> Result<TxStatus, Error> {
472        for attempt in 1..=config.attempts {
473            let response = self.trp.check_status(vec![self.hash.clone()]).await?;
474
475            if let Some(status) = response.statuses.get(&self.hash) {
476                match status.stage {
477                    TxStage::Finalized => return Ok(status.clone()),
478                    TxStage::Confirmed if matches!(target, TxStage::Confirmed) => {
479                        return Ok(status.clone())
480                    }
481                    TxStage::Dropped | TxStage::RolledBack => {
482                        return Err(Error::FinalizedFailed {
483                            hash: self.hash.clone(),
484                            stage: status.stage.clone(),
485                        });
486                    }
487                    _ => {}
488                }
489            }
490
491            if attempt < config.attempts {
492                tokio::time::sleep(config.delay).await;
493            }
494        }
495
496        Err(Error::FinalizedTimeout {
497            hash: self.hash.clone(),
498            attempts: config.attempts,
499            delay: config.delay,
500        })
501    }
502}
503
504/// Signer implementations.
505pub mod signer {
506    use super::{SignRequest, Signer};
507    use crate::core::BytesEnvelope;
508    use crate::trp::{TxWitness, WitnessType};
509    use cryptoxide::hmac::Hmac;
510    use cryptoxide::pbkdf2::pbkdf2;
511    use cryptoxide::sha2::Sha512;
512    use ed25519_bip32::{DerivationScheme, XPrv, XPRV_SIZE};
513    use pallas_addresses::{Address, ShelleyPaymentPart};
514    use pallas_crypto::hash::Hasher;
515    use pallas_crypto::key::ed25519::{SecretKey, SecretKeyExtended, Signature};
516    use thiserror::Error;
517
518    /// Errors returned by the built-in ed25519 signer.
519    #[derive(Debug, Error)]
520    pub enum SignerError {
521        /// Mnemonic phrase could not be parsed.
522        #[error("invalid mnemonic: {0}")]
523        InvalidMnemonic(bip39::Error),
524
525        /// Private key hex could not be decoded.
526        #[error("invalid private key hex: {0}")]
527        InvalidPrivateKeyHex(hex::FromHexError),
528
529        /// Private key length is not 32 bytes.
530        #[error("private key must be 32 bytes, got {0}")]
531        InvalidPrivateKeyLength(usize),
532
533        /// Transaction hash hex could not be decoded.
534        #[error("invalid tx hash hex: {0}")]
535        InvalidHashHex(hex::FromHexError),
536
537        /// Transaction hash length is not 32 bytes.
538        #[error("transaction hash must be 32 bytes, got {0}")]
539        InvalidHashLength(usize),
540
541        /// Address could not be parsed.
542        #[error("invalid address: {0}")]
543        InvalidAddress(pallas_addresses::Error),
544
545        /// Address does not contain a payment key hash.
546        #[error("address does not contain a payment key hash")]
547        UnsupportedPaymentCredential,
548
549        /// Signer key doesn't match address payment key.
550        #[error("signer key doesn't match address payment key")]
551        AddressMismatch,
552    }
553
554    /// Built-in ed25519 signer using a 32-byte private key.
555    ///
556    /// The address is required at construction and returned via `Signer::address`.
557    ///
558    /// # Example
559    ///
560    /// ```rust
561    /// use tx3_sdk::Ed25519Signer;
562    ///
563    /// let signer = Ed25519Signer::from_hex("addr_test1...", "deadbeef...")?;
564    /// # Ok::<(), tx3_sdk::Error>(())
565    /// ```
566    #[derive(Debug, Clone)]
567    pub struct Ed25519Signer {
568        address: String,
569        private_key: [u8; 32],
570    }
571
572    impl Ed25519Signer {
573        /// Creates a signer from a raw 32-byte private key and address.
574        pub fn new(address: impl Into<String>, private_key: [u8; 32]) -> Self {
575            Self {
576                address: address.into(),
577                private_key,
578            }
579        }
580
581        /// Creates a signer from a BIP39 mnemonic phrase.
582        ///
583        /// The address is required and stored on the signer.
584        pub fn from_mnemonic(
585            address: impl Into<String>,
586            phrase: &str,
587        ) -> Result<Self, SignerError> {
588            let mnemonic = bip39::Mnemonic::parse(phrase).map_err(SignerError::InvalidMnemonic)?;
589            let seed = mnemonic.to_seed("");
590
591            let mut key_array = [0u8; 32];
592            key_array.copy_from_slice(&seed[0..32]);
593
594            Ok(Self::new(address, key_array))
595        }
596
597        /// Creates a signer from a hex-encoded 32-byte private key.
598        ///
599        /// The address is required and stored on the signer.
600        pub fn from_hex(
601            address: impl Into<String>,
602            private_key_hex: &str,
603        ) -> Result<Self, SignerError> {
604            let key_bytes =
605                hex::decode(private_key_hex).map_err(SignerError::InvalidPrivateKeyHex)?;
606
607            if key_bytes.len() != 32 {
608                return Err(SignerError::InvalidPrivateKeyLength(key_bytes.len()));
609            }
610
611            let mut key_array = [0u8; 32];
612            key_array.copy_from_slice(&key_bytes);
613
614            Ok(Self::new(address, key_array))
615        }
616    }
617
618    /// Cardano signer that derives witness key from address payment part.
619    ///
620    /// This signer derives keys using the Cardano path `m/1852'/1815'/0'/0/0`.
621    ///
622    /// # Example
623    ///
624    /// ```rust
625    /// use tx3_sdk::CardanoSigner;
626    ///
627    /// let signer = CardanoSigner::from_mnemonic(
628    ///     "addr_test1...",
629    ///     "word1 word2 ... word24",
630    /// )?;
631    /// # Ok::<(), tx3_sdk::Error>(())
632    /// ```
633    #[derive(Debug, Clone)]
634    pub struct CardanoSigner {
635        address: String,
636        private_key: CardanoPrivateKey,
637        payment_key_hash: Vec<u8>,
638    }
639
640    #[derive(Debug, Clone)]
641    enum CardanoPrivateKey {
642        Normal(SecretKey),
643        Extended(SecretKeyExtended),
644    }
645
646    impl CardanoPrivateKey {
647        fn public_key_bytes(&self) -> Vec<u8> {
648            match self {
649                CardanoPrivateKey::Normal(key) => key.public_key().as_ref().to_vec(),
650                CardanoPrivateKey::Extended(key) => key.public_key().as_ref().to_vec(),
651            }
652        }
653
654        fn sign(&self, msg: &[u8]) -> Signature {
655            match self {
656                CardanoPrivateKey::Normal(key) => key.sign(msg),
657                CardanoPrivateKey::Extended(key) => key.sign(msg),
658            }
659        }
660    }
661
662    impl CardanoSigner {
663        /// Creates a Cardano signer from a raw private key and address.
664        fn new(
665            private_key: CardanoPrivateKey,
666            address: impl Into<String>,
667        ) -> Result<Self, SignerError> {
668            let address = address.into();
669            let payment_key_hash = extract_payment_key_hash(&address)?;
670            Ok(Self {
671                address,
672                private_key,
673                payment_key_hash,
674            })
675        }
676
677        /// Creates a Cardano signer from a hex-encoded private key and address.
678        pub fn from_hex(
679            address: impl Into<String>,
680            private_key_hex: &str,
681        ) -> Result<Self, SignerError> {
682            let key_bytes =
683                hex::decode(private_key_hex).map_err(SignerError::InvalidPrivateKeyHex)?;
684
685            if key_bytes.len() != 32 {
686                return Err(SignerError::InvalidPrivateKeyLength(key_bytes.len()));
687            }
688
689            let mut key_array = [0u8; 32];
690            key_array.copy_from_slice(&key_bytes);
691
692            let key: SecretKey = key_array.into();
693
694            Self::new(CardanoPrivateKey::Normal(key), address)
695        }
696
697        /// Creates a Cardano signer from a mnemonic phrase and address.
698        pub fn from_mnemonic(
699            address: impl Into<String>,
700            phrase: &str,
701        ) -> Result<Self, SignerError> {
702            let root = derive_root_xprv(phrase, "")?;
703            let payment = derive_cardano_payment_xprv(&root);
704            let key =
705                unsafe { SecretKeyExtended::from_bytes_unchecked(payment.extended_secret_key()) };
706
707            Self::new(CardanoPrivateKey::Extended(key), address)
708        }
709
710        fn verify_address_binding(&self, public_key_bytes: &[u8]) -> Result<(), SignerError> {
711            let mut hasher = Hasher::<224>::new();
712            hasher.input(public_key_bytes);
713            let digest = hasher.finalize();
714
715            if digest.as_ref() != self.payment_key_hash.as_slice() {
716                return Err(SignerError::AddressMismatch);
717            }
718
719            Ok(())
720        }
721    }
722
723    impl Signer for CardanoSigner {
724        fn address(&self) -> &str {
725            &self.address
726        }
727
728        fn sign(
729            &self,
730            request: &SignRequest,
731        ) -> Result<TxWitness, Box<dyn std::error::Error + Send + Sync>> {
732            let hash_bytes = hex::decode(&request.tx_hash_hex).map_err(|err| {
733                Box::new(SignerError::InvalidHashHex(err))
734                    as Box<dyn std::error::Error + Send + Sync>
735            })?;
736
737            if hash_bytes.len() != 32 {
738                return Err(Box::new(SignerError::InvalidHashLength(hash_bytes.len())));
739            }
740
741            let public_key_bytes = self.private_key.public_key_bytes();
742
743            let _ = self.verify_address_binding(&public_key_bytes);
744
745            let signature = self.private_key.sign(&hash_bytes);
746
747            Ok(TxWitness {
748                key: BytesEnvelope {
749                    content: hex::encode(&public_key_bytes),
750                    content_type: "hex".to_string(),
751                },
752                signature: BytesEnvelope {
753                    content: hex::encode(signature.as_ref()),
754                    content_type: "hex".to_string(),
755                },
756                witness_type: WitnessType::VKey,
757            })
758        }
759    }
760
761    fn derive_root_xprv(phrase: &str, password: &str) -> Result<XPrv, SignerError> {
762        let mnemonic = bip39::Mnemonic::parse(phrase).map_err(SignerError::InvalidMnemonic)?;
763        let entropy = mnemonic.to_entropy();
764
765        let mut pbkdf2_result = [0u8; XPRV_SIZE];
766
767        const ITER: u32 = 4096;
768
769        let mut mac = Hmac::new(Sha512::new(), password.as_bytes());
770        pbkdf2(&mut mac, &entropy, ITER, &mut pbkdf2_result);
771
772        Ok(XPrv::normalize_bytes_force3rd(pbkdf2_result))
773    }
774
775    fn derive_cardano_payment_xprv(root: &XPrv) -> XPrv {
776        const HARDENED: u32 = 0x8000_0000;
777
778        root.derive(DerivationScheme::V2, 1852 | HARDENED)
779            .derive(DerivationScheme::V2, 1815 | HARDENED)
780            .derive(DerivationScheme::V2, HARDENED)
781            .derive(DerivationScheme::V2, 0)
782            .derive(DerivationScheme::V2, 0)
783    }
784
785    fn extract_payment_key_hash(address: &str) -> Result<Vec<u8>, SignerError> {
786        let parsed = Address::from_bech32(address).map_err(SignerError::InvalidAddress)?;
787
788        let payment = match parsed {
789            Address::Shelley(addr) => addr.payment().clone(),
790            _ => return Err(SignerError::UnsupportedPaymentCredential),
791        };
792
793        match payment {
794            ShelleyPaymentPart::Key(hash) => Ok(hash.as_ref().to_vec()),
795            ShelleyPaymentPart::Script(_) => Err(SignerError::UnsupportedPaymentCredential),
796        }
797    }
798
799    impl Signer for Ed25519Signer {
800        fn address(&self) -> &str {
801            &self.address
802        }
803
804        fn sign(
805            &self,
806            request: &SignRequest,
807        ) -> Result<TxWitness, Box<dyn std::error::Error + Send + Sync>> {
808            let hash_bytes = hex::decode(&request.tx_hash_hex).map_err(|err| {
809                Box::new(SignerError::InvalidHashHex(err))
810                    as Box<dyn std::error::Error + Send + Sync>
811            })?;
812
813            if hash_bytes.len() != 32 {
814                return Err(Box::new(SignerError::InvalidHashLength(hash_bytes.len())));
815            }
816
817            let signing_key: SecretKey = self.private_key.into();
818            let public_key = signing_key.public_key();
819            let signature = signing_key.sign(&hash_bytes);
820
821            Ok(TxWitness {
822                key: BytesEnvelope {
823                    content: hex::encode(public_key.as_ref()),
824                    content_type: "hex".to_string(),
825                },
826                signature: BytesEnvelope {
827                    content: hex::encode(signature.as_ref()),
828                    content_type: "hex".to_string(),
829                },
830                witness_type: WitnessType::VKey,
831            })
832        }
833    }
834}
835
836#[cfg(test)]
837mod tests {
838    use super::*;
839    use crate::trp::{ClientOptions, WitnessType};
840
841    fn stub_trp() -> trp::Client {
842        trp::Client::new(ClientOptions {
843            endpoint: "http://localhost:0/unused".to_string(),
844            headers: None,
845        })
846    }
847
848    fn fake_witness(key_hex: &str, sig_hex: &str) -> TxWitness {
849        TxWitness {
850            key: BytesEnvelope {
851                content: key_hex.to_string(),
852                content_type: "hex".to_string(),
853            },
854            signature: BytesEnvelope {
855                content: sig_hex.to_string(),
856                content_type: "hex".to_string(),
857            },
858            witness_type: WitnessType::VKey,
859        }
860    }
861
862    fn empty_resolved() -> ResolvedTx {
863        ResolvedTx {
864            trp: stub_trp(),
865            hash: "deadbeef".to_string(),
866            tx_hex: "84a40081".to_string(),
867            signers: Vec::new(),
868            manual_witnesses: Vec::new(),
869        }
870    }
871
872    struct StubSigner {
873        address: String,
874        witness: TxWitness,
875    }
876
877    impl Signer for StubSigner {
878        fn address(&self) -> &str {
879            &self.address
880        }
881
882        fn sign(
883            &self,
884            _request: &SignRequest,
885        ) -> Result<TxWitness, Box<dyn std::error::Error + Send + Sync>> {
886            Ok(self.witness.clone())
887        }
888    }
889
890    #[test]
891    fn add_witness_only_no_signers() {
892        let witness = fake_witness("aa", "bb");
893        let signed = empty_resolved()
894            .add_witness(witness.clone())
895            .sign()
896            .expect("sign with manual witness only must succeed");
897
898        assert_eq!(signed.submit.witnesses.len(), 1);
899        assert_eq!(signed.submit.witnesses[0].key.content, witness.key.content);
900        assert_eq!(
901            signed.submit.witnesses[0].signature.content,
902            witness.signature.content
903        );
904    }
905
906    #[test]
907    fn add_witness_mixed_with_registered_signer() {
908        let registered_witness = fake_witness("11", "22");
909        let manual_witness = fake_witness("aa", "bb");
910
911        let stub = StubSigner {
912            address: "addr_test1...".to_string(),
913            witness: registered_witness.clone(),
914        };
915
916        let resolved = ResolvedTx {
917            trp: stub_trp(),
918            hash: "deadbeef".to_string(),
919            tx_hex: "84a40081".to_string(),
920            signers: vec![SignerParty {
921                name: "sender".to_string(),
922                address: stub.address.clone(),
923                signer: Arc::new(stub),
924            }],
925            manual_witnesses: Vec::new(),
926        };
927
928        let signed = resolved
929            .add_witness(manual_witness.clone())
930            .sign()
931            .expect("sign with mixed witnesses must succeed");
932
933        assert_eq!(signed.submit.witnesses.len(), 2);
934        assert_eq!(signed.submit.witnesses[0].key.content, "11");
935        assert_eq!(signed.submit.witnesses[1].key.content, "aa");
936    }
937
938    #[test]
939    fn add_witness_preserves_attach_order() {
940        let signed = empty_resolved()
941            .add_witness(fake_witness("01", "10"))
942            .add_witness(fake_witness("02", "20"))
943            .add_witness(fake_witness("03", "30"))
944            .sign()
945            .expect("sign must succeed");
946
947        let keys: Vec<&str> = signed
948            .submit
949            .witnesses
950            .iter()
951            .map(|w| w.key.content.as_str())
952            .collect();
953        assert_eq!(keys, vec!["01", "02", "03"]);
954    }
955}