1use 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#[derive(Debug, Error)]
26pub enum Error {
27 #[error(transparent)]
29 Tii(#[from] crate::tii::Error),
30
31 #[error(transparent)]
33 Trp(#[from] crate::trp::Error),
34
35 #[error("missing required params: {0:?}")]
37 MissingParams(Vec<String>),
38
39 #[error("unknown party: {0}")]
41 UnknownParty(String),
42
43 #[error("signer error: {0}")]
45 Signer(#[source] Box<dyn std::error::Error + Send + Sync>),
46
47 #[error("submit hash mismatch: expected {expected}, got {received}")]
49 SubmitHashMismatch { expected: String, received: String },
50
51 #[error("tx {hash} failed with stage {stage:?}")]
53 FinalizedFailed { hash: String, stage: TxStage },
54
55 #[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#[derive(Debug, Clone)]
68pub struct PollConfig {
69 pub attempts: u32,
71 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#[derive(Debug, Clone)]
91pub struct SignRequest {
92 pub tx_hash_hex: String,
94 pub tx_cbor_hex: String,
96}
97
98pub trait Signer: Send + Sync {
102 fn address(&self) -> &str;
104
105 fn sign(
107 &self,
108 request: &SignRequest,
109 ) -> Result<TxWitness, Box<dyn std::error::Error + Send + Sync>>;
110}
111
112#[derive(Clone)]
114pub enum Party {
115 Address(String),
117 Signer {
119 address: String,
121 signer: Arc<dyn Signer + Send + Sync>,
123 },
124}
125
126impl Party {
127 pub fn address(address: impl Into<String>) -> Self {
129 Party::Address(address.into())
130 }
131
132 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#[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 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 pub fn with_profile(mut self, profile: impl Into<String>) -> Self {
195 self.profile = Some(profile.into());
196 self
197 }
198
199 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 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 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
230pub 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 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 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 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
310pub struct ResolvedTx {
312 trp: trp::Client,
313 pub hash: String,
315 pub tx_hex: String,
317 signers: Vec<SignerParty>,
318 manual_witnesses: Vec<TxWitness>,
319}
320
321impl ResolvedTx {
322 pub fn signing_hash(&self) -> &str {
324 &self.hash
325 }
326
327 pub fn add_witness(mut self, witness: TxWitness) -> Self {
338 self.manual_witnesses.push(witness);
339 self
340 }
341
342 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#[derive(Debug, Clone)]
405pub struct WitnessInfo {
406 pub party: String,
408 pub address: String,
410 pub key: BytesEnvelope,
412 pub signature: BytesEnvelope,
414 pub witness_type: trp::WitnessType,
416 pub signed_hash: String,
418}
419
420pub struct SignedTx {
422 trp: trp::Client,
423 pub hash: String,
425 pub submit: SubmitParams,
427 witnesses_info: Vec<WitnessInfo>,
428}
429
430impl SignedTx {
431 pub fn witnesses(&self) -> &[WitnessInfo] {
433 &self.witnesses_info
434 }
435 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
453pub struct SubmittedTx {
455 trp: trp::Client,
456 pub hash: String,
458}
459
460impl SubmittedTx {
461 pub async fn wait_for_confirmed(&self, config: PollConfig) -> Result<TxStatus, Error> {
463 self.wait_for_stage(config, TxStage::Confirmed).await
464 }
465
466 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
504pub 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 #[derive(Debug, Error)]
520 pub enum SignerError {
521 #[error("invalid mnemonic: {0}")]
523 InvalidMnemonic(bip39::Error),
524
525 #[error("invalid private key hex: {0}")]
527 InvalidPrivateKeyHex(hex::FromHexError),
528
529 #[error("private key must be 32 bytes, got {0}")]
531 InvalidPrivateKeyLength(usize),
532
533 #[error("invalid tx hash hex: {0}")]
535 InvalidHashHex(hex::FromHexError),
536
537 #[error("transaction hash must be 32 bytes, got {0}")]
539 InvalidHashLength(usize),
540
541 #[error("invalid address: {0}")]
543 InvalidAddress(pallas_addresses::Error),
544
545 #[error("address does not contain a payment key hash")]
547 UnsupportedPaymentCredential,
548
549 #[error("signer key doesn't match address payment key")]
551 AddressMismatch,
552 }
553
554 #[derive(Debug, Clone)]
567 pub struct Ed25519Signer {
568 address: String,
569 private_key: [u8; 32],
570 }
571
572 impl Ed25519Signer {
573 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 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 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 #[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 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 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 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}