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/// A signer capable of producing TRP witnesses.
85///
86/// Signers are address-aware and must return the address they correspond to.
87pub trait Signer: Send + Sync {
88    /// Returns the address associated with this signer.
89    fn address(&self) -> &str;
90
91    /// Signs a transaction hash given as hex-encoded bytes.
92    fn sign(&self, tx_hash: &str) -> Result<TxWitness, Box<dyn std::error::Error + Send + Sync>>;
93}
94
95/// A party referenced by the protocol.
96#[derive(Clone)]
97pub enum Party {
98    /// Read-only party with a known address.
99    Address(String),
100    /// Party capable of signing transactions.
101    Signer {
102        /// Party address (used for invocation args).
103        address: String,
104        /// Signer implementation.
105        signer: Arc<dyn Signer + Send + Sync>,
106    },
107}
108
109impl Party {
110    /// Creates a read-only party from an address.
111    pub fn address(address: impl Into<String>) -> Self {
112        Party::Address(address.into())
113    }
114
115    /// Creates a signer party from a signer.
116    ///
117    /// The party address is taken from the signer itself.
118    ///
119    /// # Example
120    ///
121    /// ```rust
122    /// use tx3_sdk::{CardanoSigner, Party};
123    ///
124    /// let signer = CardanoSigner::from_hex("addr_test1...", "deadbeef...")?;
125    /// let party = Party::signer(signer);
126    /// # Ok::<(), tx3_sdk::Error>(())
127    /// ```
128    pub fn signer(signer: impl Signer + 'static) -> Self {
129        Party::Signer {
130            address: signer.address().to_string(),
131            signer: Arc::new(signer),
132        }
133    }
134
135    fn address_value(&self) -> &str {
136        match self {
137            Party::Address(address) => address,
138            Party::Signer { address, .. } => address,
139        }
140    }
141
142    fn signer_party(&self, name: &str) -> Option<SignerParty> {
143        match self {
144            Party::Signer { address, signer } => Some(SignerParty {
145                name: name.to_string(),
146                address: address.clone(),
147                signer: Arc::clone(signer),
148            }),
149            _ => None,
150        }
151    }
152}
153
154/// High-level client that ties a protocol to a TRP client.
155#[derive(Clone)]
156pub struct Tx3Client {
157    protocol: Arc<Protocol>,
158    trp: trp::Client,
159    parties: HashMap<String, Party>,
160    profile: Option<String>,
161}
162
163impl Tx3Client {
164    /// Creates a new facade client.
165    pub fn new(protocol: Protocol, trp: trp::Client) -> Self {
166        Self {
167            protocol: Arc::new(protocol),
168            trp,
169            parties: HashMap::new(),
170            profile: None,
171        }
172    }
173
174    /// Sets the profile for all invocations created by this client.
175    ///
176    /// This profile is applied to every invocation created by the client.
177    pub fn with_profile(mut self, profile: impl Into<String>) -> Self {
178        self.profile = Some(profile.into());
179        self
180    }
181
182    /// Attaches a party definition to this client.
183    pub fn with_party(mut self, name: impl Into<String>, party: Party) -> Self {
184        self.parties.insert(name.into().to_lowercase(), party);
185        self
186    }
187
188    /// Attaches multiple party definitions to this client.
189    pub fn with_parties<I, K>(mut self, parties: I) -> Self
190    where
191        I: IntoIterator<Item = (K, Party)>,
192        K: Into<String>,
193    {
194        for (name, party) in parties {
195            self.parties.insert(name.into().to_lowercase(), party);
196        }
197        self
198    }
199
200    /// Starts building a transaction invocation.
201    pub fn tx(&self, name: impl Into<String>) -> TxBuilder {
202        TxBuilder {
203            protocol: Arc::clone(&self.protocol),
204            trp: self.trp.clone(),
205            tx_name: name.into(),
206            args: ArgMap::new(),
207            parties: self.parties.clone(),
208            profile: self.profile.clone(),
209        }
210    }
211}
212
213/// Builder for transaction invocation.
214pub struct TxBuilder {
215    protocol: Arc<Protocol>,
216    trp: trp::Client,
217    tx_name: String,
218    args: ArgMap,
219    parties: HashMap<String, Party>,
220    profile: Option<String>,
221}
222
223impl TxBuilder {
224    /// Adds a single argument (case-insensitive name).
225    pub fn arg(mut self, name: &str, value: impl Into<Value>) -> Self {
226        self.args.insert(name.to_lowercase(), value.into());
227        self
228    }
229
230    /// Adds multiple arguments (case-insensitive names).
231    pub fn args(mut self, args: ArgMap) -> Self {
232        for (key, value) in args {
233            self.args.insert(key.to_lowercase(), value);
234        }
235        self
236    }
237
238    /// Resolves the transaction using the TRP client.
239    pub async fn resolve(self) -> Result<ResolvedTx, Error> {
240        let mut invocation = self
241            .protocol
242            .invoke(&self.tx_name, self.profile.as_deref())?;
243
244        let known_parties: HashSet<String> = self
245            .protocol
246            .parties()
247            .keys()
248            .map(|key| key.to_lowercase())
249            .collect();
250
251        for (name, party) in &self.parties {
252            if !known_parties.contains(name) {
253                return Err(Error::UnknownParty(name.clone()));
254            }
255
256            invocation.set_arg(
257                name,
258                serde_json::Value::String(party.address_value().to_string()),
259            );
260        }
261
262        invocation.set_args(self.args);
263
264        let mut missing: Vec<String> = invocation
265            .unspecified_params()
266            .map(|(key, _)| key.clone())
267            .collect();
268
269        if !missing.is_empty() {
270            missing.sort();
271            return Err(Error::MissingParams(missing));
272        }
273
274        let resolve_params = invocation.into_resolve_request()?;
275        let envelope = self.trp.resolve(resolve_params).await?;
276
277        let signers = self
278            .parties
279            .iter()
280            .filter_map(|(name, party)| party.signer_party(name))
281            .collect();
282
283        Ok(ResolvedTx {
284            trp: self.trp,
285            hash: envelope.hash,
286            tx_hex: envelope.tx,
287            signers,
288        })
289    }
290}
291
292/// A resolved transaction ready for signing.
293pub struct ResolvedTx {
294    trp: trp::Client,
295    /// Transaction hash.
296    pub hash: String,
297    /// Hex-encoded CBOR transaction bytes.
298    pub tx_hex: String,
299    signers: Vec<SignerParty>,
300}
301
302impl ResolvedTx {
303    /// Returns the transaction hash that signers will sign.
304    pub fn signing_hash(&self) -> &str {
305        &self.hash
306    }
307
308    /// Signs the transaction with every signer party.
309    pub fn sign(self) -> Result<SignedTx, Error> {
310        let mut witnesses = Vec::with_capacity(self.signers.len());
311        let mut witnesses_info = Vec::with_capacity(self.signers.len());
312
313        for signer_party in &self.signers {
314            let witness = signer_party
315                .signer
316                .sign(&self.hash)
317                .map_err(Error::Signer)?;
318            witnesses_info.push(WitnessInfo {
319                party: signer_party.name.clone(),
320                address: signer_party.address.clone(),
321                key: witness.key.clone(),
322                signature: witness.signature.clone(),
323                witness_type: witness.witness_type.clone(),
324                signed_hash: self.hash.clone(),
325            });
326            witnesses.push(witness);
327        }
328
329        let submit = SubmitParams {
330            tx: BytesEnvelope {
331                content: self.tx_hex,
332                content_type: "hex".to_string(),
333            },
334            witnesses,
335        };
336
337        Ok(SignedTx {
338            trp: self.trp,
339            hash: self.hash,
340            submit,
341            witnesses_info,
342        })
343    }
344}
345
346/// Witness payloads for submission.
347#[derive(Debug, Clone)]
348pub struct WitnessInfo {
349    /// Party name from the protocol.
350    pub party: String,
351    /// Party address used in invocation args.
352    pub address: String,
353    /// Public key envelope sent to the server.
354    pub key: BytesEnvelope,
355    /// Signature envelope sent to the server.
356    pub signature: BytesEnvelope,
357    /// Witness type.
358    pub witness_type: trp::WitnessType,
359    /// Transaction hash that was signed.
360    pub signed_hash: String,
361}
362
363/// A signed transaction ready for submission.
364pub struct SignedTx {
365    trp: trp::Client,
366    /// Resolved transaction hash.
367    pub hash: String,
368    /// Submit parameters including witnesses.
369    pub submit: SubmitParams,
370    witnesses_info: Vec<WitnessInfo>,
371}
372
373impl SignedTx {
374    /// Returns witness payloads for submission.
375    pub fn witnesses(&self) -> &[WitnessInfo] {
376        &self.witnesses_info
377    }
378    /// Submits the signed transaction.
379    pub async fn submit(self) -> Result<SubmittedTx, Error> {
380        let response = self.trp.submit(self.submit).await?;
381
382        if response.hash != self.hash {
383            return Err(Error::SubmitHashMismatch {
384                expected: self.hash,
385                received: response.hash,
386            });
387        }
388
389        Ok(SubmittedTx {
390            trp: self.trp,
391            hash: response.hash,
392        })
393    }
394}
395
396/// A submitted transaction that can be polled for status.
397pub struct SubmittedTx {
398    trp: trp::Client,
399    /// Submitted transaction hash.
400    pub hash: String,
401}
402
403impl SubmittedTx {
404    /// Polls check-status until the transaction is confirmed or fails.
405    pub async fn wait_for_confirmed(&self, config: PollConfig) -> Result<TxStatus, Error> {
406        self.wait_for_stage(config, TxStage::Confirmed).await
407    }
408
409    /// Polls check-status until the transaction is finalized or fails.
410    pub async fn wait_for_finalized(&self, config: PollConfig) -> Result<TxStatus, Error> {
411        self.wait_for_stage(config, TxStage::Finalized).await
412    }
413
414    async fn wait_for_stage(&self, config: PollConfig, target: TxStage) -> Result<TxStatus, Error> {
415        for attempt in 1..=config.attempts {
416            let response = self.trp.check_status(vec![self.hash.clone()]).await?;
417
418            if let Some(status) = response.statuses.get(&self.hash) {
419                match status.stage {
420                    TxStage::Finalized => return Ok(status.clone()),
421                    TxStage::Confirmed if matches!(target, TxStage::Confirmed) => {
422                        return Ok(status.clone())
423                    }
424                    TxStage::Dropped | TxStage::RolledBack => {
425                        return Err(Error::FinalizedFailed {
426                            hash: self.hash.clone(),
427                            stage: status.stage.clone(),
428                        });
429                    }
430                    _ => {}
431                }
432            }
433
434            if attempt < config.attempts {
435                tokio::time::sleep(config.delay).await;
436            }
437        }
438
439        Err(Error::FinalizedTimeout {
440            hash: self.hash.clone(),
441            attempts: config.attempts,
442            delay: config.delay,
443        })
444    }
445}
446
447/// Signer implementations.
448pub mod signer {
449    use super::Signer;
450    use crate::core::BytesEnvelope;
451    use crate::trp::{TxWitness, WitnessType};
452    use cryptoxide::hmac::Hmac;
453    use cryptoxide::pbkdf2::pbkdf2;
454    use cryptoxide::sha2::Sha512;
455    use ed25519_bip32::{DerivationScheme, XPrv, XPRV_SIZE};
456    use pallas_addresses::{Address, ShelleyPaymentPart};
457    use pallas_crypto::hash::Hasher;
458    use pallas_crypto::key::ed25519::{SecretKey, SecretKeyExtended, Signature};
459    use thiserror::Error;
460
461    /// Errors returned by the built-in ed25519 signer.
462    #[derive(Debug, Error)]
463    pub enum SignerError {
464        /// Mnemonic phrase could not be parsed.
465        #[error("invalid mnemonic: {0}")]
466        InvalidMnemonic(bip39::Error),
467
468        /// Private key hex could not be decoded.
469        #[error("invalid private key hex: {0}")]
470        InvalidPrivateKeyHex(hex::FromHexError),
471
472        /// Private key length is not 32 bytes.
473        #[error("private key must be 32 bytes, got {0}")]
474        InvalidPrivateKeyLength(usize),
475
476        /// Transaction hash hex could not be decoded.
477        #[error("invalid tx hash hex: {0}")]
478        InvalidHashHex(hex::FromHexError),
479
480        /// Transaction hash length is not 32 bytes.
481        #[error("transaction hash must be 32 bytes, got {0}")]
482        InvalidHashLength(usize),
483
484        /// Address could not be parsed.
485        #[error("invalid address: {0}")]
486        InvalidAddress(pallas_addresses::Error),
487
488        /// Address does not contain a payment key hash.
489        #[error("address does not contain a payment key hash")]
490        UnsupportedPaymentCredential,
491
492        /// Signer key doesn't match address payment key.
493        #[error("signer key doesn't match address payment key")]
494        AddressMismatch,
495    }
496
497    /// Built-in ed25519 signer using a 32-byte private key.
498    ///
499    /// The address is required at construction and returned via `Signer::address`.
500    ///
501    /// # Example
502    ///
503    /// ```rust
504    /// use tx3_sdk::Ed25519Signer;
505    ///
506    /// let signer = Ed25519Signer::from_hex("addr_test1...", "deadbeef...")?;
507    /// # Ok::<(), tx3_sdk::Error>(())
508    /// ```
509    #[derive(Debug, Clone)]
510    pub struct Ed25519Signer {
511        address: String,
512        private_key: [u8; 32],
513    }
514
515    impl Ed25519Signer {
516        /// Creates a signer from a raw 32-byte private key and address.
517        pub fn new(address: impl Into<String>, private_key: [u8; 32]) -> Self {
518            Self {
519                address: address.into(),
520                private_key,
521            }
522        }
523
524        /// Creates a signer from a BIP39 mnemonic phrase.
525        ///
526        /// The address is required and stored on the signer.
527        pub fn from_mnemonic(
528            address: impl Into<String>,
529            phrase: &str,
530        ) -> Result<Self, SignerError> {
531            let mnemonic = bip39::Mnemonic::parse(phrase).map_err(SignerError::InvalidMnemonic)?;
532            let seed = mnemonic.to_seed("");
533
534            let mut key_array = [0u8; 32];
535            key_array.copy_from_slice(&seed[0..32]);
536
537            Ok(Self::new(address, key_array))
538        }
539
540        /// Creates a signer from a hex-encoded 32-byte private key.
541        ///
542        /// The address is required and stored on the signer.
543        pub fn from_hex(
544            address: impl Into<String>,
545            private_key_hex: &str,
546        ) -> Result<Self, SignerError> {
547            let key_bytes =
548                hex::decode(private_key_hex).map_err(SignerError::InvalidPrivateKeyHex)?;
549
550            if key_bytes.len() != 32 {
551                return Err(SignerError::InvalidPrivateKeyLength(key_bytes.len()));
552            }
553
554            let mut key_array = [0u8; 32];
555            key_array.copy_from_slice(&key_bytes);
556
557            Ok(Self::new(address, key_array))
558        }
559    }
560
561    /// Cardano signer that derives witness key from address payment part.
562    ///
563    /// This signer derives keys using the Cardano path `m/1852'/1815'/0'/0/0`.
564    ///
565    /// # Example
566    ///
567    /// ```rust
568    /// use tx3_sdk::CardanoSigner;
569    ///
570    /// let signer = CardanoSigner::from_mnemonic(
571    ///     "addr_test1...",
572    ///     "word1 word2 ... word24",
573    /// )?;
574    /// # Ok::<(), tx3_sdk::Error>(())
575    /// ```
576    #[derive(Debug, Clone)]
577    pub struct CardanoSigner {
578        address: String,
579        private_key: CardanoPrivateKey,
580        payment_key_hash: Vec<u8>,
581    }
582
583    #[derive(Debug, Clone)]
584    enum CardanoPrivateKey {
585        Normal(SecretKey),
586        Extended(SecretKeyExtended),
587    }
588
589    impl CardanoPrivateKey {
590        fn public_key_bytes(&self) -> Vec<u8> {
591            match self {
592                CardanoPrivateKey::Normal(key) => key.public_key().as_ref().to_vec(),
593                CardanoPrivateKey::Extended(key) => key.public_key().as_ref().to_vec(),
594            }
595        }
596
597        fn sign(&self, msg: &[u8]) -> Signature {
598            match self {
599                CardanoPrivateKey::Normal(key) => key.sign(msg),
600                CardanoPrivateKey::Extended(key) => key.sign(msg),
601            }
602        }
603    }
604
605    impl CardanoSigner {
606        /// Creates a Cardano signer from a raw private key and address.
607        fn new(
608            private_key: CardanoPrivateKey,
609            address: impl Into<String>,
610        ) -> Result<Self, SignerError> {
611            let address = address.into();
612            let payment_key_hash = extract_payment_key_hash(&address)?;
613            Ok(Self {
614                address,
615                private_key,
616                payment_key_hash,
617            })
618        }
619
620        /// Creates a Cardano signer from a hex-encoded private key and address.
621        pub fn from_hex(
622            address: impl Into<String>,
623            private_key_hex: &str,
624        ) -> Result<Self, SignerError> {
625            let key_bytes =
626                hex::decode(private_key_hex).map_err(SignerError::InvalidPrivateKeyHex)?;
627
628            if key_bytes.len() != 32 {
629                return Err(SignerError::InvalidPrivateKeyLength(key_bytes.len()));
630            }
631
632            let mut key_array = [0u8; 32];
633            key_array.copy_from_slice(&key_bytes);
634
635            let key: SecretKey = key_array.into();
636
637            Self::new(CardanoPrivateKey::Normal(key), address)
638        }
639
640        /// Creates a Cardano signer from a mnemonic phrase and address.
641        pub fn from_mnemonic(
642            address: impl Into<String>,
643            phrase: &str,
644        ) -> Result<Self, SignerError> {
645            let root = derive_root_xprv(phrase, "")?;
646            let payment = derive_cardano_payment_xprv(&root);
647            let key =
648                unsafe { SecretKeyExtended::from_bytes_unchecked(payment.extended_secret_key()) };
649
650            Self::new(CardanoPrivateKey::Extended(key), address)
651        }
652
653        fn verify_address_binding(&self, public_key_bytes: &[u8]) -> Result<(), SignerError> {
654            let mut hasher = Hasher::<224>::new();
655            hasher.input(public_key_bytes);
656            let digest = hasher.finalize();
657
658            if digest.as_ref() != self.payment_key_hash.as_slice() {
659                return Err(SignerError::AddressMismatch);
660            }
661
662            Ok(())
663        }
664    }
665
666    impl Signer for CardanoSigner {
667        fn address(&self) -> &str {
668            &self.address
669        }
670
671        fn sign(
672            &self,
673            tx_hash: &str,
674        ) -> Result<TxWitness, Box<dyn std::error::Error + Send + Sync>> {
675            let hash_bytes = hex::decode(tx_hash).map_err(|err| {
676                Box::new(SignerError::InvalidHashHex(err))
677                    as Box<dyn std::error::Error + Send + Sync>
678            })?;
679
680            if hash_bytes.len() != 32 {
681                return Err(Box::new(SignerError::InvalidHashLength(hash_bytes.len())));
682            }
683
684            let public_key_bytes = self.private_key.public_key_bytes();
685
686            let _ = self.verify_address_binding(&public_key_bytes);
687
688            let signature = self.private_key.sign(&hash_bytes);
689
690            Ok(TxWitness {
691                key: BytesEnvelope {
692                    content: hex::encode(&public_key_bytes),
693                    content_type: "hex".to_string(),
694                },
695                signature: BytesEnvelope {
696                    content: hex::encode(signature.as_ref()),
697                    content_type: "hex".to_string(),
698                },
699                witness_type: WitnessType::VKey,
700            })
701        }
702    }
703
704    fn derive_root_xprv(phrase: &str, password: &str) -> Result<XPrv, SignerError> {
705        let mnemonic = bip39::Mnemonic::parse(phrase).map_err(SignerError::InvalidMnemonic)?;
706        let entropy = mnemonic.to_entropy();
707
708        let mut pbkdf2_result = [0u8; XPRV_SIZE];
709
710        const ITER: u32 = 4096;
711
712        let mut mac = Hmac::new(Sha512::new(), password.as_bytes());
713        pbkdf2(&mut mac, &entropy, ITER, &mut pbkdf2_result);
714
715        Ok(XPrv::normalize_bytes_force3rd(pbkdf2_result))
716    }
717
718    fn derive_cardano_payment_xprv(root: &XPrv) -> XPrv {
719        const HARDENED: u32 = 0x8000_0000;
720
721        root.derive(DerivationScheme::V2, 1852 | HARDENED)
722            .derive(DerivationScheme::V2, 1815 | HARDENED)
723            .derive(DerivationScheme::V2, HARDENED)
724            .derive(DerivationScheme::V2, 0)
725            .derive(DerivationScheme::V2, 0)
726    }
727
728    fn extract_payment_key_hash(address: &str) -> Result<Vec<u8>, SignerError> {
729        let parsed = Address::from_bech32(address).map_err(SignerError::InvalidAddress)?;
730
731        let payment = match parsed {
732            Address::Shelley(addr) => addr.payment().clone(),
733            _ => return Err(SignerError::UnsupportedPaymentCredential),
734        };
735
736        match payment {
737            ShelleyPaymentPart::Key(hash) => Ok(hash.as_ref().to_vec()),
738            ShelleyPaymentPart::Script(_) => Err(SignerError::UnsupportedPaymentCredential),
739        }
740    }
741
742    impl Signer for Ed25519Signer {
743        fn address(&self) -> &str {
744            &self.address
745        }
746
747        fn sign(
748            &self,
749            tx_hash: &str,
750        ) -> Result<TxWitness, Box<dyn std::error::Error + Send + Sync>> {
751            let hash_bytes = hex::decode(tx_hash).map_err(|err| {
752                Box::new(SignerError::InvalidHashHex(err))
753                    as Box<dyn std::error::Error + Send + Sync>
754            })?;
755
756            if hash_bytes.len() != 32 {
757                return Err(Box::new(SignerError::InvalidHashLength(hash_bytes.len())));
758            }
759
760            let signing_key: SecretKey = self.private_key.into();
761            let public_key = signing_key.public_key();
762            let signature = signing_key.sign(&hash_bytes);
763
764            Ok(TxWitness {
765                key: BytesEnvelope {
766                    content: hex::encode(public_key.as_ref()),
767                    content_type: "hex".to_string(),
768                },
769                signature: BytesEnvelope {
770                    content: hex::encode(signature.as_ref()),
771                    content_type: "hex".to_string(),
772                },
773                witness_type: WitnessType::VKey,
774            })
775        }
776    }
777}