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::{self, ParamMap, 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 tx_params: HashMap<String, ParamMap>,
210 known_parties: HashSet<String>,
211 trp: trp::Client,
212 bound_parties: HashMap<String, Party>,
213 selected_profile: Option<Profile>,
214 env_overrides: EnvMap,
215}
216
217impl Tx3Client {
218 #[allow(clippy::too_many_arguments)]
223 pub(crate) fn from_parts(
224 transactions: HashMap<String, TirEnvelope>,
225 tx_params: HashMap<String, ParamMap>,
226 known_parties: HashSet<String>,
227 trp: trp::Client,
228 bound_parties: HashMap<String, Party>,
229 selected_profile: Option<Profile>,
230 env_overrides: EnvMap,
231 ) -> Self {
232 let known_parties = known_parties
233 .into_iter()
234 .map(|name| name.to_lowercase())
235 .collect();
236 Self {
237 transactions,
238 tx_params,
239 known_parties,
240 trp,
241 bound_parties,
242 selected_profile,
243 env_overrides,
244 }
245 }
246
247 pub fn with_party(mut self, name: impl Into<String>, party: Party) -> 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(mut self, name: impl Into<String>, party: Party) -> Self {
271 self.bound_parties.insert(name.into().to_lowercase(), party);
272 self
273 }
274
275 pub fn with_parties<I, K>(mut self, parties: I) -> Result<Self, Error>
277 where
278 I: IntoIterator<Item = (K, Party)>,
279 K: Into<String>,
280 {
281 for (name, party) in parties {
282 self = self.with_party(name, party)?;
283 }
284 Ok(self)
285 }
286
287 pub fn tx(&self, name: impl Into<String>) -> Result<TxBuilder, Error> {
294 let name = name.into();
295 let tir = self
296 .transactions
297 .get(&name)
298 .cloned()
299 .ok_or_else(|| Error::UnknownTx(name.clone()))?;
300
301 let params = self.tx_params.get(&name).cloned().unwrap_or_default();
302
303 Ok(TxBuilder::new(tir, self.trp.clone())
304 .params(params)
305 .env(self.env())
306 .parties(self.merged_parties()))
307 }
308
309 fn env(&self) -> EnvMap {
310 let mut env = self
311 .selected_profile
312 .as_ref()
313 .map(|profile| profile.environment.clone())
314 .unwrap_or_default();
315 for (key, value) in &self.env_overrides {
316 env.insert(key.clone(), value.clone());
317 }
318 env
319 }
320
321 fn merged_parties(&self) -> HashMap<String, Party> {
322 let mut merged = HashMap::new();
323 if let Some(profile) = &self.selected_profile {
324 for (name, address) in &profile.parties {
325 merged.insert(name.to_lowercase(), Party::address(address.clone()));
326 }
327 }
328 for (name, party) in &self.bound_parties {
329 merged.insert(name.clone(), party.clone());
330 }
331 merged
332 }
333}
334
335pub struct Tx3ClientBuilder {
356 transactions: HashMap<String, TirEnvelope>,
357 tx_params: HashMap<String, ParamMap>,
358 profiles: HashMap<String, Profile>,
359 known_parties: HashSet<String>,
360 trp_options: Option<trp::ClientOptions>,
361 profile: Option<String>,
362 parties: HashMap<String, Party>,
363 unchecked_parties: HashMap<String, Party>,
364 env_overrides: EnvMap,
365}
366
367impl Tx3ClientBuilder {
368 pub fn from_parts(
375 transactions: HashMap<String, TirEnvelope>,
376 profiles: HashMap<String, Profile>,
377 known_parties: HashSet<String>,
378 ) -> Self {
379 let known_parties = known_parties
380 .into_iter()
381 .map(|name| name.to_lowercase())
382 .collect();
383 Self {
384 transactions,
385 tx_params: HashMap::new(),
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 tx_params = protocol
404 .txs()
405 .keys()
406 .filter_map(|name| {
407 protocol
408 .tx_params(name)
409 .ok()
410 .map(|params| (name.clone(), params))
411 })
412 .collect();
413
414 let profiles = protocol
415 .profiles()
416 .iter()
417 .map(|(name, profile)| {
418 let environment = profile.environment.as_object().cloned().unwrap_or_default();
419 (
420 name.clone(),
421 Profile {
422 environment,
423 parties: profile.parties.clone(),
424 },
425 )
426 })
427 .collect();
428
429 let known_parties = protocol.parties().keys().cloned().collect();
430
431 let mut builder = Self::from_parts(transactions, profiles, known_parties);
432 builder.tx_params = tx_params;
433 builder
434 }
435
436 pub fn trp(mut self, opts: trp::ClientOptions) -> Self {
438 self.trp_options = Some(opts);
439 self
440 }
441
442 pub fn trp_endpoint(mut self, url: impl Into<String>) -> Self {
445 self.trp_options = Some(trp::ClientOptions {
446 endpoint: url.into(),
447 headers: None,
448 });
449 self
450 }
451
452 pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
456 let opts = self.trp_options.get_or_insert_with(|| trp::ClientOptions {
457 endpoint: String::new(),
458 headers: None,
459 });
460 opts.headers
461 .get_or_insert_with(HashMap::new)
462 .insert(key.into(), value.into());
463 self
464 }
465
466 pub fn with_tx_params(mut self, tx: impl Into<String>, params: ParamMap) -> Self {
476 self.tx_params.insert(tx.into(), params);
477 self
478 }
479
480 pub fn with_profile(mut self, name: impl Into<String>) -> Self {
482 self.profile = Some(name.into());
483 self
484 }
485
486 pub fn with_party(mut self, name: impl Into<String>, party: Party) -> Self {
489 self.parties.insert(name.into().to_lowercase(), party);
490 self
491 }
492
493 pub fn with_party_unchecked(mut self, name: impl Into<String>, party: Party) -> Self {
502 self.unchecked_parties
503 .insert(name.into().to_lowercase(), party);
504 self
505 }
506
507 pub fn with_parties<I, K>(mut self, parties: I) -> Self
509 where
510 I: IntoIterator<Item = (K, Party)>,
511 K: Into<String>,
512 {
513 for (name, party) in parties {
514 self = self.with_party(name, party);
515 }
516 self
517 }
518
519 pub fn with_env_value(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
522 self.env_overrides.insert(key.into(), value.into());
523 self
524 }
525
526 pub fn build(self) -> Result<Tx3Client, Error> {
536 let trp_options = self.trp_options.ok_or(Error::MissingTrpEndpoint)?;
537 if trp_options.endpoint.is_empty() {
538 return Err(Error::MissingTrpEndpoint);
539 }
540
541 let selected_profile = match self.profile {
542 Some(name) => Some(
543 self.profiles
544 .get(&name)
545 .cloned()
546 .ok_or(Error::UnknownProfile(name))?,
547 ),
548 None => None,
549 };
550
551 for name in self.parties.keys() {
552 if !self.known_parties.contains(name) {
553 return Err(Error::UnknownParty(name.clone()));
554 }
555 }
556
557 let trp = trp::Client::new(trp_options);
558
559 let mut bound_parties = self.parties;
560 bound_parties.extend(self.unchecked_parties);
561
562 Ok(Tx3Client::from_parts(
563 self.transactions,
564 self.tx_params,
565 self.known_parties,
566 trp,
567 bound_parties,
568 selected_profile,
569 self.env_overrides,
570 ))
571 }
572}
573
574fn build_resolve_params(
588 tir: TirEnvelope,
589 env: EnvMap,
590 parties: &HashMap<String, Party>,
591 args: ArgMap,
592 params: &ParamMap,
593) -> Result<ResolveParams, Error> {
594 let mut merged = ArgMap::new();
595 merged.extend(env);
596 for (name, party) in parties {
597 merged.insert(
598 name.clone(),
599 Value::String(party.address_value().to_string()),
600 );
601 }
602 merged.extend(args);
603
604 let encoded = merged
605 .into_iter()
606 .map(|(key, value)| {
607 match params
608 .iter()
609 .find(|(name, _)| name.to_lowercase() == key.to_lowercase())
610 {
611 Some((_, ty)) => {
612 let value = tii::encode(ty, &value).map_err(tii::Error::from)?;
613 Ok((key, value))
614 }
615 None => Ok((key, value)),
616 }
617 })
618 .collect::<Result<_, tii::Error>>()?;
619
620 Ok(ResolveParams {
621 tir,
622 args: encoded,
623 env: None,
624 })
625}
626
627pub struct TxBuilder {
634 tir: TirEnvelope,
635 env: EnvMap,
636 trp: trp::Client,
637 args: ArgMap,
638 parties: HashMap<String, Party>,
639 params: ParamMap,
640}
641
642impl TxBuilder {
643 pub fn new(tir: TirEnvelope, trp: trp::Client) -> Self {
651 TxBuilder {
652 tir,
653 env: EnvMap::new(),
654 trp,
655 args: ArgMap::new(),
656 parties: HashMap::new(),
657 params: ParamMap::new(),
658 }
659 }
660
661 pub fn env(mut self, env: EnvMap) -> Self {
663 self.env = env;
664 self
665 }
666
667 pub fn params(mut self, params: ParamMap) -> Self {
671 self.params = params;
672 self
673 }
674
675 pub fn parties(mut self, parties: HashMap<String, Party>) -> Self {
680 for (name, party) in parties {
681 self.parties.insert(name.to_lowercase(), party);
682 }
683 self
684 }
685
686 pub fn arg(mut self, name: &str, value: impl Into<Value>) -> Self {
688 self.args.insert(name.to_lowercase(), value.into());
689 self
690 }
691
692 pub fn args(mut self, args: ArgMap) -> Self {
694 for (key, value) in args {
695 self.args.insert(key.to_lowercase(), value);
696 }
697 self
698 }
699
700 pub async fn resolve(self) -> Result<ResolvedTx, Error> {
702 let TxBuilder {
703 tir,
704 env,
705 trp,
706 args,
707 parties,
708 params,
709 } = self;
710
711 let resolve_params = build_resolve_params(tir, env, &parties, args, ¶ms)?;
712
713 let envelope = trp.resolve(resolve_params).await?;
714
715 let signers = parties
716 .iter()
717 .filter_map(|(name, party)| party.signer_party(name))
718 .collect();
719
720 Ok(ResolvedTx {
721 trp,
722 hash: envelope.hash,
723 tx_hex: envelope.tx,
724 signers,
725 manual_witnesses: Vec::new(),
726 })
727 }
728}
729
730pub struct ResolvedTx {
732 trp: trp::Client,
733 pub hash: String,
735 pub tx_hex: String,
737 signers: Vec<SignerParty>,
738 manual_witnesses: Vec<TxWitness>,
739}
740
741impl ResolvedTx {
742 pub fn signing_hash(&self) -> &str {
744 &self.hash
745 }
746
747 pub fn add_witness(mut self, witness: TxWitness) -> Self {
758 self.manual_witnesses.push(witness);
759 self
760 }
761
762 pub fn sign(self) -> Result<SignedTx, Error> {
769 let total = self.signers.len() + self.manual_witnesses.len();
770 let mut witnesses = Vec::with_capacity(total);
771 let mut witnesses_info = Vec::with_capacity(total);
772
773 let request = SignRequest {
774 tx_hash_hex: self.hash.clone(),
775 tx_cbor_hex: self.tx_hex.clone(),
776 };
777
778 for signer_party in &self.signers {
779 let witness = signer_party.signer.sign(&request).map_err(Error::Signer)?;
780 witnesses_info.push(WitnessInfo {
781 party: signer_party.name.clone(),
782 address: signer_party.address.clone(),
783 key: witness.key.clone(),
784 signature: witness.signature.clone(),
785 witness_type: witness.witness_type.clone(),
786 signed_hash: self.hash.clone(),
787 });
788 witnesses.push(witness);
789 }
790
791 for witness in self.manual_witnesses {
792 witnesses_info.push(WitnessInfo {
793 party: "<external>".to_string(),
794 address: String::new(),
795 key: witness.key.clone(),
796 signature: witness.signature.clone(),
797 witness_type: witness.witness_type.clone(),
798 signed_hash: self.hash.clone(),
799 });
800 witnesses.push(witness);
801 }
802
803 let submit = SubmitParams {
804 tx: BytesEnvelope {
805 content: self.tx_hex,
806 content_type: "hex".to_string(),
807 },
808 witnesses,
809 };
810
811 Ok(SignedTx {
812 trp: self.trp,
813 hash: self.hash,
814 submit,
815 witnesses_info,
816 })
817 }
818}
819
820#[derive(Debug, Clone)]
822pub struct WitnessInfo {
823 pub party: String,
825 pub address: String,
827 pub key: BytesEnvelope,
829 pub signature: BytesEnvelope,
831 pub witness_type: trp::WitnessType,
833 pub signed_hash: String,
835}
836
837pub struct SignedTx {
839 trp: trp::Client,
840 pub hash: String,
842 pub submit: SubmitParams,
844 witnesses_info: Vec<WitnessInfo>,
845}
846
847impl SignedTx {
848 pub fn witnesses(&self) -> &[WitnessInfo] {
850 &self.witnesses_info
851 }
852 pub async fn submit(self) -> Result<SubmittedTx, Error> {
854 let response = self.trp.submit(self.submit).await?;
855
856 if response.hash != self.hash {
857 return Err(Error::SubmitHashMismatch {
858 expected: self.hash,
859 received: response.hash,
860 });
861 }
862
863 Ok(SubmittedTx {
864 trp: self.trp,
865 hash: response.hash,
866 })
867 }
868}
869
870pub struct SubmittedTx {
872 trp: trp::Client,
873 pub hash: String,
875}
876
877impl SubmittedTx {
878 pub async fn wait_for_confirmed(&self, config: PollConfig) -> Result<TxStatus, Error> {
880 self.wait_for_stage(config, TxStage::Confirmed).await
881 }
882
883 pub async fn wait_for_finalized(&self, config: PollConfig) -> Result<TxStatus, Error> {
885 self.wait_for_stage(config, TxStage::Finalized).await
886 }
887
888 async fn wait_for_stage(&self, config: PollConfig, target: TxStage) -> Result<TxStatus, Error> {
889 for attempt in 1..=config.attempts {
890 let response = self.trp.check_status(vec![self.hash.clone()]).await?;
891
892 if let Some(status) = response.statuses.get(&self.hash) {
893 match status.stage {
894 TxStage::Finalized => return Ok(status.clone()),
895 TxStage::Confirmed if matches!(target, TxStage::Confirmed) => {
896 return Ok(status.clone())
897 }
898 TxStage::Dropped | TxStage::RolledBack => {
899 return Err(Error::FinalizedFailed {
900 hash: self.hash.clone(),
901 stage: status.stage.clone(),
902 });
903 }
904 _ => {}
905 }
906 }
907
908 if attempt < config.attempts {
909 tokio::time::sleep(config.delay).await;
910 }
911 }
912
913 Err(Error::FinalizedTimeout {
914 hash: self.hash.clone(),
915 attempts: config.attempts,
916 delay: config.delay,
917 })
918 }
919}
920
921pub mod signer {
923 use super::{SignRequest, Signer};
924 use crate::core::BytesEnvelope;
925 use crate::trp::{TxWitness, WitnessType};
926 use cryptoxide::hmac::Hmac;
927 use cryptoxide::pbkdf2::pbkdf2;
928 use cryptoxide::sha2::Sha512;
929 use ed25519_bip32::{DerivationScheme, XPrv, XPRV_SIZE};
930 use pallas_addresses::{Address, ShelleyPaymentPart};
931 use pallas_crypto::hash::Hasher;
932 use pallas_crypto::key::ed25519::{SecretKey, SecretKeyExtended, Signature};
933 use thiserror::Error;
934
935 #[derive(Debug, Error)]
937 pub enum SignerError {
938 #[error("invalid mnemonic: {0}")]
940 InvalidMnemonic(bip39::Error),
941
942 #[error("invalid private key hex: {0}")]
944 InvalidPrivateKeyHex(hex::FromHexError),
945
946 #[error("private key must be 32 bytes, got {0}")]
948 InvalidPrivateKeyLength(usize),
949
950 #[error("invalid tx hash hex: {0}")]
952 InvalidHashHex(hex::FromHexError),
953
954 #[error("transaction hash must be 32 bytes, got {0}")]
956 InvalidHashLength(usize),
957
958 #[error("invalid address: {0}")]
960 InvalidAddress(pallas_addresses::Error),
961
962 #[error("address does not contain a payment key hash")]
964 UnsupportedPaymentCredential,
965
966 #[error("signer key doesn't match address payment key")]
968 AddressMismatch,
969 }
970
971 #[derive(Debug, Clone)]
984 pub struct Ed25519Signer {
985 address: String,
986 private_key: [u8; 32],
987 }
988
989 impl Ed25519Signer {
990 pub fn new(address: impl Into<String>, private_key: [u8; 32]) -> Self {
992 Self {
993 address: address.into(),
994 private_key,
995 }
996 }
997
998 pub fn from_mnemonic(
1002 address: impl Into<String>,
1003 phrase: &str,
1004 ) -> Result<Self, SignerError> {
1005 let mnemonic = bip39::Mnemonic::parse(phrase).map_err(SignerError::InvalidMnemonic)?;
1006 let seed = mnemonic.to_seed("");
1007
1008 let mut key_array = [0u8; 32];
1009 key_array.copy_from_slice(&seed[0..32]);
1010
1011 Ok(Self::new(address, key_array))
1012 }
1013
1014 pub fn from_hex(
1018 address: impl Into<String>,
1019 private_key_hex: &str,
1020 ) -> Result<Self, SignerError> {
1021 let key_bytes =
1022 hex::decode(private_key_hex).map_err(SignerError::InvalidPrivateKeyHex)?;
1023
1024 if key_bytes.len() != 32 {
1025 return Err(SignerError::InvalidPrivateKeyLength(key_bytes.len()));
1026 }
1027
1028 let mut key_array = [0u8; 32];
1029 key_array.copy_from_slice(&key_bytes);
1030
1031 Ok(Self::new(address, key_array))
1032 }
1033 }
1034
1035 #[derive(Debug, Clone)]
1051 pub struct CardanoSigner {
1052 address: String,
1053 private_key: CardanoPrivateKey,
1054 payment_key_hash: Vec<u8>,
1055 }
1056
1057 #[derive(Debug, Clone)]
1058 enum CardanoPrivateKey {
1059 Normal(SecretKey),
1060 Extended(SecretKeyExtended),
1061 }
1062
1063 impl CardanoPrivateKey {
1064 fn public_key_bytes(&self) -> Vec<u8> {
1065 match self {
1066 CardanoPrivateKey::Normal(key) => key.public_key().as_ref().to_vec(),
1067 CardanoPrivateKey::Extended(key) => key.public_key().as_ref().to_vec(),
1068 }
1069 }
1070
1071 fn sign(&self, msg: &[u8]) -> Signature {
1072 match self {
1073 CardanoPrivateKey::Normal(key) => key.sign(msg),
1074 CardanoPrivateKey::Extended(key) => key.sign(msg),
1075 }
1076 }
1077 }
1078
1079 impl CardanoSigner {
1080 fn new(
1082 private_key: CardanoPrivateKey,
1083 address: impl Into<String>,
1084 ) -> Result<Self, SignerError> {
1085 let address = address.into();
1086 let payment_key_hash = extract_payment_key_hash(&address)?;
1087 Ok(Self {
1088 address,
1089 private_key,
1090 payment_key_hash,
1091 })
1092 }
1093
1094 pub fn from_hex(
1096 address: impl Into<String>,
1097 private_key_hex: &str,
1098 ) -> Result<Self, SignerError> {
1099 let key_bytes =
1100 hex::decode(private_key_hex).map_err(SignerError::InvalidPrivateKeyHex)?;
1101
1102 if key_bytes.len() != 32 {
1103 return Err(SignerError::InvalidPrivateKeyLength(key_bytes.len()));
1104 }
1105
1106 let mut key_array = [0u8; 32];
1107 key_array.copy_from_slice(&key_bytes);
1108
1109 let key: SecretKey = key_array.into();
1110
1111 Self::new(CardanoPrivateKey::Normal(key), address)
1112 }
1113
1114 pub fn from_mnemonic(
1116 address: impl Into<String>,
1117 phrase: &str,
1118 ) -> Result<Self, SignerError> {
1119 let root = derive_root_xprv(phrase, "")?;
1120 let payment = derive_cardano_payment_xprv(&root);
1121 let key =
1122 unsafe { SecretKeyExtended::from_bytes_unchecked(payment.extended_secret_key()) };
1123
1124 Self::new(CardanoPrivateKey::Extended(key), address)
1125 }
1126
1127 fn verify_address_binding(&self, public_key_bytes: &[u8]) -> Result<(), SignerError> {
1128 let mut hasher = Hasher::<224>::new();
1129 hasher.input(public_key_bytes);
1130 let digest = hasher.finalize();
1131
1132 if digest.as_ref() != self.payment_key_hash.as_slice() {
1133 return Err(SignerError::AddressMismatch);
1134 }
1135
1136 Ok(())
1137 }
1138 }
1139
1140 impl Signer for CardanoSigner {
1141 fn address(&self) -> &str {
1142 &self.address
1143 }
1144
1145 fn sign(
1146 &self,
1147 request: &SignRequest,
1148 ) -> Result<TxWitness, Box<dyn std::error::Error + Send + Sync>> {
1149 let hash_bytes = hex::decode(&request.tx_hash_hex).map_err(|err| {
1150 Box::new(SignerError::InvalidHashHex(err))
1151 as Box<dyn std::error::Error + Send + Sync>
1152 })?;
1153
1154 if hash_bytes.len() != 32 {
1155 return Err(Box::new(SignerError::InvalidHashLength(hash_bytes.len())));
1156 }
1157
1158 let public_key_bytes = self.private_key.public_key_bytes();
1159
1160 let _ = self.verify_address_binding(&public_key_bytes);
1161
1162 let signature = self.private_key.sign(&hash_bytes);
1163
1164 Ok(TxWitness {
1165 key: BytesEnvelope {
1166 content: hex::encode(&public_key_bytes),
1167 content_type: "hex".to_string(),
1168 },
1169 signature: BytesEnvelope {
1170 content: hex::encode(signature.as_ref()),
1171 content_type: "hex".to_string(),
1172 },
1173 witness_type: WitnessType::VKey,
1174 })
1175 }
1176 }
1177
1178 fn derive_root_xprv(phrase: &str, password: &str) -> Result<XPrv, SignerError> {
1179 let mnemonic = bip39::Mnemonic::parse(phrase).map_err(SignerError::InvalidMnemonic)?;
1180 let entropy = mnemonic.to_entropy();
1181
1182 let mut pbkdf2_result = [0u8; XPRV_SIZE];
1183
1184 const ITER: u32 = 4096;
1185
1186 let mut mac = Hmac::new(Sha512::new(), password.as_bytes());
1187 pbkdf2(&mut mac, &entropy, ITER, &mut pbkdf2_result);
1188
1189 Ok(XPrv::normalize_bytes_force3rd(pbkdf2_result))
1190 }
1191
1192 fn derive_cardano_payment_xprv(root: &XPrv) -> XPrv {
1193 const HARDENED: u32 = 0x8000_0000;
1194
1195 root.derive(DerivationScheme::V2, 1852 | HARDENED)
1196 .derive(DerivationScheme::V2, 1815 | HARDENED)
1197 .derive(DerivationScheme::V2, HARDENED)
1198 .derive(DerivationScheme::V2, 0)
1199 .derive(DerivationScheme::V2, 0)
1200 }
1201
1202 fn extract_payment_key_hash(address: &str) -> Result<Vec<u8>, SignerError> {
1203 let parsed = Address::from_bech32(address).map_err(SignerError::InvalidAddress)?;
1204
1205 let payment = match parsed {
1206 Address::Shelley(addr) => addr.payment().clone(),
1207 _ => return Err(SignerError::UnsupportedPaymentCredential),
1208 };
1209
1210 match payment {
1211 ShelleyPaymentPart::Key(hash) => Ok(hash.as_ref().to_vec()),
1212 ShelleyPaymentPart::Script(_) => Err(SignerError::UnsupportedPaymentCredential),
1213 }
1214 }
1215
1216 impl Signer for Ed25519Signer {
1217 fn address(&self) -> &str {
1218 &self.address
1219 }
1220
1221 fn sign(
1222 &self,
1223 request: &SignRequest,
1224 ) -> Result<TxWitness, Box<dyn std::error::Error + Send + Sync>> {
1225 let hash_bytes = hex::decode(&request.tx_hash_hex).map_err(|err| {
1226 Box::new(SignerError::InvalidHashHex(err))
1227 as Box<dyn std::error::Error + Send + Sync>
1228 })?;
1229
1230 if hash_bytes.len() != 32 {
1231 return Err(Box::new(SignerError::InvalidHashLength(hash_bytes.len())));
1232 }
1233
1234 let signing_key: SecretKey = self.private_key.into();
1235 let public_key = signing_key.public_key();
1236 let signature = signing_key.sign(&hash_bytes);
1237
1238 Ok(TxWitness {
1239 key: BytesEnvelope {
1240 content: hex::encode(public_key.as_ref()),
1241 content_type: "hex".to_string(),
1242 },
1243 signature: BytesEnvelope {
1244 content: hex::encode(signature.as_ref()),
1245 content_type: "hex".to_string(),
1246 },
1247 witness_type: WitnessType::VKey,
1248 })
1249 }
1250 }
1251}
1252
1253#[cfg(test)]
1254mod tests {
1255 use super::*;
1256 use crate::trp::{ClientOptions, WitnessType};
1257
1258 fn stub_trp() -> trp::Client {
1259 trp::Client::new(ClientOptions {
1260 endpoint: "http://localhost:0/unused".to_string(),
1261 headers: None,
1262 })
1263 }
1264
1265 fn fake_witness(key_hex: &str, sig_hex: &str) -> TxWitness {
1266 TxWitness {
1267 key: BytesEnvelope {
1268 content: key_hex.to_string(),
1269 content_type: "hex".to_string(),
1270 },
1271 signature: BytesEnvelope {
1272 content: sig_hex.to_string(),
1273 content_type: "hex".to_string(),
1274 },
1275 witness_type: WitnessType::VKey,
1276 }
1277 }
1278
1279 fn empty_resolved() -> ResolvedTx {
1280 ResolvedTx {
1281 trp: stub_trp(),
1282 hash: "deadbeef".to_string(),
1283 tx_hex: "84a40081".to_string(),
1284 signers: Vec::new(),
1285 manual_witnesses: Vec::new(),
1286 }
1287 }
1288
1289 struct StubSigner {
1290 address: String,
1291 witness: TxWitness,
1292 }
1293
1294 impl Signer for StubSigner {
1295 fn address(&self) -> &str {
1296 &self.address
1297 }
1298
1299 fn sign(
1300 &self,
1301 _request: &SignRequest,
1302 ) -> Result<TxWitness, Box<dyn std::error::Error + Send + Sync>> {
1303 Ok(self.witness.clone())
1304 }
1305 }
1306
1307 #[test]
1308 fn add_witness_only_no_signers() {
1309 let witness = fake_witness("aa", "bb");
1310 let signed = empty_resolved()
1311 .add_witness(witness.clone())
1312 .sign()
1313 .expect("sign with manual witness only must succeed");
1314
1315 assert_eq!(signed.submit.witnesses.len(), 1);
1316 assert_eq!(signed.submit.witnesses[0].key.content, witness.key.content);
1317 assert_eq!(
1318 signed.submit.witnesses[0].signature.content,
1319 witness.signature.content
1320 );
1321 }
1322
1323 #[test]
1324 fn add_witness_mixed_with_registered_signer() {
1325 let registered_witness = fake_witness("11", "22");
1326 let manual_witness = fake_witness("aa", "bb");
1327
1328 let stub = StubSigner {
1329 address: "addr_test1...".to_string(),
1330 witness: registered_witness.clone(),
1331 };
1332
1333 let resolved = ResolvedTx {
1334 trp: stub_trp(),
1335 hash: "deadbeef".to_string(),
1336 tx_hex: "84a40081".to_string(),
1337 signers: vec![SignerParty {
1338 name: "sender".to_string(),
1339 address: stub.address.clone(),
1340 signer: Arc::new(stub),
1341 }],
1342 manual_witnesses: Vec::new(),
1343 };
1344
1345 let signed = resolved
1346 .add_witness(manual_witness.clone())
1347 .sign()
1348 .expect("sign with mixed witnesses must succeed");
1349
1350 assert_eq!(signed.submit.witnesses.len(), 2);
1351 assert_eq!(signed.submit.witnesses[0].key.content, "11");
1352 assert_eq!(signed.submit.witnesses[1].key.content, "aa");
1353 }
1354
1355 #[test]
1356 fn add_witness_preserves_attach_order() {
1357 let signed = empty_resolved()
1358 .add_witness(fake_witness("01", "10"))
1359 .add_witness(fake_witness("02", "20"))
1360 .add_witness(fake_witness("03", "30"))
1361 .sign()
1362 .expect("sign must succeed");
1363
1364 let keys: Vec<&str> = signed
1365 .submit
1366 .witnesses
1367 .iter()
1368 .map(|w| w.key.content.as_str())
1369 .collect();
1370 assert_eq!(keys, vec!["01", "02", "03"]);
1371 }
1372
1373 fn sample_tir() -> TirEnvelope {
1374 TirEnvelope {
1375 content: "abcd".to_string(),
1376 encoding: crate::core::TirEncoding::Hex,
1377 version: "v1beta0".to_string(),
1378 }
1379 }
1380
1381 #[test]
1382 fn resolve_params_merges_env_parties_and_args() {
1383 let mut env = EnvMap::new();
1384 env.insert("network".to_string(), serde_json::json!("testnet"));
1385
1386 let mut parties = HashMap::new();
1387 parties.insert("receiver".to_string(), Party::address("addr_receiver"));
1388
1389 let mut args = ArgMap::new();
1390 args.insert("quantity".to_string(), serde_json::json!(10_000_000));
1391
1392 let params =
1393 build_resolve_params(sample_tir(), env, &parties, args, &ParamMap::new()).unwrap();
1394
1395 assert_eq!(params.env, None);
1396 assert_eq!(params.tir.content, "abcd");
1397 assert_eq!(
1398 params.args.get("network").unwrap(),
1399 &serde_json::json!("testnet")
1400 );
1401 assert_eq!(
1402 params.args.get("receiver").unwrap(),
1403 &serde_json::json!("addr_receiver")
1404 );
1405 assert_eq!(
1406 params.args.get("quantity").unwrap(),
1407 &serde_json::json!(10_000_000)
1408 );
1409 }
1410
1411 #[test]
1412 fn resolve_params_args_override_env() {
1413 let mut env = EnvMap::new();
1414 env.insert("quantity".to_string(), serde_json::json!(1));
1415
1416 let mut args = ArgMap::new();
1417 args.insert("quantity".to_string(), serde_json::json!(999));
1418
1419 let params =
1420 build_resolve_params(sample_tir(), env, &HashMap::new(), args, &ParamMap::new())
1421 .unwrap();
1422
1423 assert_eq!(
1424 params.args.get("quantity").unwrap(),
1425 &serde_json::json!(999)
1426 );
1427 }
1428
1429 #[test]
1430 fn resolve_params_uses_signer_party_address() {
1431 let stub = StubSigner {
1432 address: "addr_signer".to_string(),
1433 witness: fake_witness("aa", "bb"),
1434 };
1435
1436 let mut parties = HashMap::new();
1437 parties.insert("sender".to_string(), Party::signer(stub));
1438
1439 let params = build_resolve_params(
1440 sample_tir(),
1441 EnvMap::new(),
1442 &parties,
1443 ArgMap::new(),
1444 &ParamMap::new(),
1445 )
1446 .unwrap();
1447
1448 assert_eq!(
1449 params.args.get("sender").unwrap(),
1450 &serde_json::json!("addr_signer")
1451 );
1452 }
1453
1454 fn param_of(schema: serde_json::Value) -> crate::tii::ParamType {
1457 crate::tii::ParamType::from_json_schema(&schema, &HashMap::new())
1458 }
1459
1460 #[test]
1461 fn resolve_params_encode_typed_args_to_tagged_wire_form() {
1462 let mut params = ParamMap::new();
1468 params.insert(
1469 "participants".to_string(),
1470 param_of(serde_json::json!({
1471 "type": "array",
1472 "items": { "$ref": "https://tx3.land/specs/v1beta0/tii#/$defs/Bytes" }
1473 })),
1474 );
1475 params.insert(
1476 "head_id".to_string(),
1477 param_of(
1478 serde_json::json!({ "$ref": "https://tx3.land/specs/v1beta0/tii#/$defs/Bytes" }),
1479 ),
1480 );
1481
1482 let mut args = ArgMap::new();
1483 args.insert("participants".to_string(), serde_json::json!([[1, 2]]));
1484 args.insert("head_id".to_string(), serde_json::json!("abcd0123"));
1485
1486 let resolve =
1487 build_resolve_params(sample_tir(), EnvMap::new(), &HashMap::new(), args, ¶ms)
1488 .unwrap();
1489
1490 assert_eq!(
1491 resolve.args.get("participants").unwrap(),
1492 &serde_json::json!({ "list": [{ "bytes": "0x0102" }] })
1493 );
1494 assert_eq!(
1496 resolve.args.get("head_id").unwrap(),
1497 &serde_json::json!("abcd0123")
1498 );
1499 }
1500
1501 #[test]
1502 fn resolve_params_match_param_names_case_insensitively() {
1503 let mut params = ParamMap::new();
1506 params.insert(
1507 "Participants".to_string(),
1508 param_of(serde_json::json!({
1509 "type": "array",
1510 "items": { "type": "integer" }
1511 })),
1512 );
1513
1514 let mut args = ArgMap::new();
1515 args.insert("participants".to_string(), serde_json::json!([7]));
1516
1517 let resolve =
1518 build_resolve_params(sample_tir(), EnvMap::new(), &HashMap::new(), args, ¶ms)
1519 .unwrap();
1520
1521 assert_eq!(
1522 resolve.args.get("participants").unwrap(),
1523 &serde_json::json!({ "list": [{ "int": 7 }] })
1524 );
1525 }
1526
1527 #[test]
1528 fn resolve_params_pass_unmapped_args_through() {
1529 let mut args = ArgMap::new();
1530 args.insert("mystery".to_string(), serde_json::json!([[1, 2]]));
1531
1532 let resolve = build_resolve_params(
1533 sample_tir(),
1534 EnvMap::new(),
1535 &HashMap::new(),
1536 args,
1537 &ParamMap::new(),
1538 )
1539 .unwrap();
1540
1541 assert_eq!(
1543 resolve.args.get("mystery").unwrap(),
1544 &serde_json::json!([[1, 2]])
1545 );
1546 }
1547
1548 #[test]
1549 fn resolve_params_surface_encode_errors_preflight() {
1550 let mut params = ParamMap::new();
1551 params.insert(
1552 "name".to_string(),
1553 param_of(
1554 serde_json::json!({ "$ref": "https://tx3.land/specs/v1beta0/tii#/$defs/Bytes" }),
1555 ),
1556 );
1557
1558 let mut args = ArgMap::new();
1559 args.insert("name".to_string(), serde_json::json!(42));
1560
1561 let result =
1562 build_resolve_params(sample_tir(), EnvMap::new(), &HashMap::new(), args, ¶ms);
1563
1564 assert!(matches!(
1565 result,
1566 Err(Error::Tii(crate::tii::Error::EncodeArg(_)))
1567 ));
1568 }
1569
1570 #[test]
1571 fn client_tx_wires_protocol_params_into_builder() {
1572 let manifest_dir = env!("CARGO_MANIFEST_DIR");
1576 let tii = format!("{manifest_dir}/tests/fixtures/transfer.tii");
1577 let protocol = crate::tii::Protocol::from_file(&tii).unwrap();
1578
1579 let client = protocol
1580 .client()
1581 .trp_endpoint("http://localhost:0")
1582 .build()
1583 .unwrap();
1584
1585 let builder = client.tx("transfer").unwrap();
1586 assert!(
1587 !builder.params.is_empty(),
1588 "tx builder must receive the protocol's param types"
1589 );
1590 }
1591}