1use 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#[derive(Debug, Error)]
27pub enum Error {
28 #[error(transparent)]
30 Tii(#[from] crate::tii::Error),
31
32 #[error(transparent)]
34 Trp(#[from] crate::trp::Error),
35
36 #[error("unknown transaction: {0}")]
38 UnknownTx(String),
39
40 #[error("unknown profile: {0}")]
42 UnknownProfile(String),
43
44 #[error("unknown party: {0}")]
46 UnknownParty(String),
47
48 #[error("TRP endpoint not configured")]
50 MissingTrpEndpoint,
51
52 #[error("signer error: {0}")]
54 Signer(#[source] Box<dyn std::error::Error + Send + Sync>),
55
56 #[error("submit hash mismatch: expected {expected}, got {received}")]
58 SubmitHashMismatch { expected: String, received: String },
59
60 #[error("tx {hash} failed with stage {stage:?}")]
62 FinalizedFailed { hash: String, stage: TxStage },
63
64 #[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#[derive(Debug, Clone)]
77pub struct PollConfig {
78 pub attempts: u32,
80 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#[derive(Debug, Clone)]
100pub struct SignRequest {
101 pub tx_hash_hex: String,
103 pub tx_cbor_hex: String,
105}
106
107pub trait Signer: Send + Sync {
111 fn address(&self) -> &str;
113
114 fn sign(
116 &self,
117 request: &SignRequest,
118 ) -> Result<TxWitness, Box<dyn std::error::Error + Send + Sync>>;
119}
120
121#[derive(Clone)]
123pub enum Party {
124 Address(String),
126 Signer {
128 address: String,
130 signer: Arc<dyn Signer + Send + Sync>,
132 },
133}
134
135impl Party {
136 pub fn address(address: impl Into<String>) -> Self {
138 Party::Address(address.into())
139 }
140
141 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#[derive(Debug, Clone, Default, Deserialize)]
188pub struct Profile {
189 #[serde(default)]
191 pub environment: EnvMap,
192 #[serde(default)]
194 pub parties: HashMap<String, String>,
195}
196
197#[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 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 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 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 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 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
337pub 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 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 pub fn trp(mut self, opts: trp::ClientOptions) -> Self {
426 self.trp_options = Some(opts);
427 self
428 }
429
430 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 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 pub fn with_profile(mut self, name: impl Into<String>) -> Self {
456 self.profile = Some(name.into());
457 self
458 }
459
460 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 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 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 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 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
551fn 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
580pub 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 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 pub fn env(mut self, env: EnvMap) -> Self {
614 self.env = env;
615 self
616 }
617
618 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 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 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 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
672pub struct ResolvedTx {
674 trp: trp::Client,
675 pub hash: String,
677 pub tx_hex: String,
679 signers: Vec<SignerParty>,
680 manual_witnesses: Vec<TxWitness>,
681}
682
683impl ResolvedTx {
684 pub fn signing_hash(&self) -> &str {
686 &self.hash
687 }
688
689 pub fn add_witness(mut self, witness: TxWitness) -> Self {
700 self.manual_witnesses.push(witness);
701 self
702 }
703
704 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#[derive(Debug, Clone)]
767pub struct WitnessInfo {
768 pub party: String,
770 pub address: String,
772 pub key: BytesEnvelope,
774 pub signature: BytesEnvelope,
776 pub witness_type: trp::WitnessType,
778 pub signed_hash: String,
780}
781
782pub struct SignedTx {
784 trp: trp::Client,
785 pub hash: String,
787 pub submit: SubmitParams,
789 witnesses_info: Vec<WitnessInfo>,
790}
791
792impl SignedTx {
793 pub fn witnesses(&self) -> &[WitnessInfo] {
795 &self.witnesses_info
796 }
797 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
815pub struct SubmittedTx {
817 trp: trp::Client,
818 pub hash: String,
820}
821
822impl SubmittedTx {
823 pub async fn wait_for_confirmed(&self, config: PollConfig) -> Result<TxStatus, Error> {
825 self.wait_for_stage(config, TxStage::Confirmed).await
826 }
827
828 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
866pub 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 #[derive(Debug, Error)]
882 pub enum SignerError {
883 #[error("invalid mnemonic: {0}")]
885 InvalidMnemonic(bip39::Error),
886
887 #[error("invalid private key hex: {0}")]
889 InvalidPrivateKeyHex(hex::FromHexError),
890
891 #[error("private key must be 32 bytes, got {0}")]
893 InvalidPrivateKeyLength(usize),
894
895 #[error("invalid tx hash hex: {0}")]
897 InvalidHashHex(hex::FromHexError),
898
899 #[error("transaction hash must be 32 bytes, got {0}")]
901 InvalidHashLength(usize),
902
903 #[error("invalid address: {0}")]
905 InvalidAddress(pallas_addresses::Error),
906
907 #[error("address does not contain a payment key hash")]
909 UnsupportedPaymentCredential,
910
911 #[error("signer key doesn't match address payment key")]
913 AddressMismatch,
914 }
915
916 #[derive(Debug, Clone)]
929 pub struct Ed25519Signer {
930 address: String,
931 private_key: [u8; 32],
932 }
933
934 impl Ed25519Signer {
935 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 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 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 #[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 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 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 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}