Skip to main content

bsv/wallet/
proto_wallet.rs

1//! ProtoWallet: crypto-only wallet wrapping KeyDeriver.
2//!
3//! ProtoWallet provides sign, verify, encrypt, decrypt, HMAC, and key linkage
4//! revelation operations. It implements WalletInterface so it can be used
5//! anywhere a wallet is needed (e.g. auth, certificates, testing).
6//! Unsupported methods (transactions, outputs, certificates, blockchain queries)
7//! return `WalletError::NotImplemented`.
8
9use crate::primitives::ecdsa::{ecdsa_sign, ecdsa_verify};
10use crate::primitives::hash::{sha256, sha256_hmac};
11use crate::primitives::point::Point;
12use crate::primitives::private_key::PrivateKey;
13use crate::primitives::public_key::PublicKey;
14use crate::primitives::schnorr::schnorr_generate_proof;
15use crate::primitives::signature::Signature;
16use crate::wallet::error::WalletError;
17use crate::wallet::interfaces::{
18    AbortActionArgs, AbortActionResult, AcquireCertificateArgs, AuthenticatedResult, Certificate,
19    CreateActionArgs, CreateActionResult, CreateHmacArgs, CreateHmacResult, CreateSignatureArgs,
20    CreateSignatureResult, DecryptArgs, DecryptResult, DiscoverByAttributesArgs,
21    DiscoverByIdentityKeyArgs, DiscoverCertificatesResult, EncryptArgs, EncryptResult,
22    GetHeaderArgs, GetHeaderResult, GetHeightResult, GetNetworkResult, GetPublicKeyArgs,
23    GetPublicKeyResult, GetVersionResult, InternalizeActionArgs, InternalizeActionResult,
24    ListActionsArgs, ListActionsResult, ListCertificatesArgs, ListCertificatesResult,
25    ListOutputsArgs, ListOutputsResult, ProveCertificateArgs, ProveCertificateResult,
26    RelinquishCertificateArgs, RelinquishCertificateResult, RelinquishOutputArgs,
27    RelinquishOutputResult, RevealCounterpartyKeyLinkageArgs, RevealCounterpartyKeyLinkageResult,
28    RevealSpecificKeyLinkageArgs, RevealSpecificKeyLinkageResult, SignActionArgs, SignActionResult,
29    VerifyHmacArgs, VerifyHmacResult, VerifySignatureArgs, VerifySignatureResult, WalletInterface,
30};
31use crate::wallet::key_deriver::KeyDeriver;
32use crate::wallet::types::{Counterparty, CounterpartyType, Protocol};
33
34/// Result of revealing counterparty key linkage.
35pub struct RevealCounterpartyResult {
36    /// The prover's identity public key.
37    pub prover: PublicKey,
38    /// The counterparty's public key.
39    pub counterparty: PublicKey,
40    /// The verifier's public key (who can decrypt the revelation).
41    pub verifier: PublicKey,
42    /// ISO 8601 timestamp of the revelation.
43    pub revelation_time: String,
44    /// Encrypted shared secret (linkage), encrypted for the verifier.
45    pub encrypted_linkage: Vec<u8>,
46    /// Encrypted proof of the linkage, encrypted for the verifier.
47    pub encrypted_linkage_proof: Vec<u8>,
48}
49
50/// Result of revealing specific key linkage.
51pub struct RevealSpecificResult {
52    /// Encrypted specific secret, encrypted for the verifier.
53    pub encrypted_linkage: Vec<u8>,
54    /// Encrypted proof bytes, encrypted for the verifier.
55    pub encrypted_linkage_proof: Vec<u8>,
56    /// The prover's identity public key.
57    pub prover: PublicKey,
58    /// The verifier's public key.
59    pub verifier: PublicKey,
60    /// The counterparty's public key.
61    pub counterparty: PublicKey,
62    /// The protocol used for this specific derivation.
63    pub protocol: Protocol,
64    /// The key ID used for this specific derivation.
65    pub key_id: String,
66    /// Proof type (0 = no proof for specific linkage).
67    pub proof_type: u8,
68}
69
70/// ProtoWallet is a crypto-only wallet wrapping KeyDeriver.
71///
72/// It provides foundational cryptographic operations: key derivation,
73/// signing, verification, encryption, decryption, HMAC, and key linkage
74/// revelation. Unlike a full wallet, it does not create transactions,
75/// manage outputs, or interact with the blockchain.
76pub struct ProtoWallet {
77    key_deriver: KeyDeriver,
78}
79
80impl ProtoWallet {
81    /// Create a new ProtoWallet from a private key.
82    pub fn new(private_key: PrivateKey) -> Self {
83        ProtoWallet {
84            key_deriver: KeyDeriver::new(private_key),
85        }
86    }
87
88    /// Create a new ProtoWallet from an existing KeyDeriver.
89    pub fn from_key_deriver(kd: KeyDeriver) -> Self {
90        ProtoWallet { key_deriver: kd }
91    }
92
93    /// Create an "anyone" ProtoWallet using the special anyone key (PrivateKey(1)).
94    pub fn anyone() -> Self {
95        ProtoWallet {
96            key_deriver: KeyDeriver::new_anyone(),
97        }
98    }
99
100    /// Get a public key, either the identity key or a derived key.
101    ///
102    /// If `identity_key` is true, returns the root identity public key
103    /// (protocol, key_id, counterparty, for_self are ignored).
104    /// Otherwise, derives a public key using the given parameters.
105    /// If counterparty is Uninitialized, it defaults to Self_.
106    pub fn get_public_key_sync(
107        &self,
108        protocol: &Protocol,
109        key_id: &str,
110        counterparty: &Counterparty,
111        for_self: bool,
112        identity_key: bool,
113    ) -> Result<PublicKey, WalletError> {
114        if identity_key {
115            return Ok(self.key_deriver.identity_key());
116        }
117
118        if protocol.protocol.is_empty() || key_id.is_empty() {
119            return Err(WalletError::InvalidParameter(
120                "protocolID and keyID are required if identityKey is false".to_string(),
121            ));
122        }
123
124        let effective = self.default_counterparty(counterparty, CounterpartyType::Self_);
125        self.key_deriver
126            .derive_public_key(protocol, key_id, &effective, for_self)
127    }
128
129    /// Create an ECDSA signature over data or a pre-computed hash.
130    ///
131    /// If `data` is provided, hashes it with SHA-256 then signs.
132    /// If `hash_to_directly_sign` is provided, signs it directly.
133    /// Returns the DER-encoded signature bytes.
134    pub fn create_signature_sync(
135        &self,
136        data: Option<&[u8]>,
137        hash_to_directly_sign: Option<&[u8]>,
138        protocol: &Protocol,
139        key_id: &str,
140        counterparty: &Counterparty,
141    ) -> Result<Vec<u8>, WalletError> {
142        let effective = self.default_counterparty(counterparty, CounterpartyType::Anyone);
143        let derived_key = self
144            .key_deriver
145            .derive_private_key(protocol, key_id, &effective)?;
146
147        let hash = if let Some(h) = hash_to_directly_sign {
148            if h.len() != 32 {
149                return Err(WalletError::InvalidParameter(
150                    "hash_to_directly_sign must be exactly 32 bytes".to_string(),
151                ));
152            }
153            let mut arr = [0u8; 32];
154            arr.copy_from_slice(h);
155            arr
156        } else if let Some(d) = data {
157            sha256(d)
158        } else {
159            return Err(WalletError::InvalidParameter(
160                "either data or hash_to_directly_sign must be provided".to_string(),
161            ));
162        };
163        let sig = ecdsa_sign(&hash, derived_key.bn(), true)?;
164        Ok(sig.to_der())
165    }
166
167    /// Verify an ECDSA signature over data or a pre-computed hash.
168    ///
169    /// If `data` is provided, hashes it with SHA-256 then verifies.
170    /// If `hash_to_directly_verify` is provided, verifies against it directly.
171    pub fn verify_signature_sync(
172        &self,
173        data: Option<&[u8]>,
174        hash_to_directly_verify: Option<&[u8]>,
175        signature: &[u8],
176        protocol: &Protocol,
177        key_id: &str,
178        counterparty: &Counterparty,
179        for_self: bool,
180    ) -> Result<bool, WalletError> {
181        let effective = self.default_counterparty(counterparty, CounterpartyType::Self_);
182        let derived_pub = self
183            .key_deriver
184            .derive_public_key(protocol, key_id, &effective, for_self)?;
185
186        let sig = Signature::from_der(signature)?;
187        let hash = if let Some(h) = hash_to_directly_verify {
188            if h.len() != 32 {
189                return Err(WalletError::InvalidParameter(
190                    "hash_to_directly_verify must be exactly 32 bytes".to_string(),
191                ));
192            }
193            let mut arr = [0u8; 32];
194            arr.copy_from_slice(h);
195            arr
196        } else if let Some(d) = data {
197            sha256(d)
198        } else {
199            return Err(WalletError::InvalidParameter(
200                "either data or hash_to_directly_verify must be provided".to_string(),
201            ));
202        };
203        Ok(ecdsa_verify(&hash, &sig, derived_pub.point()))
204    }
205
206    /// Encrypt plaintext using a derived symmetric key (AES-GCM).
207    ///
208    /// Derives a symmetric key from the protocol, key ID, and counterparty,
209    /// then encrypts the plaintext. Returns IV || ciphertext || auth tag.
210    pub fn encrypt_sync(
211        &self,
212        plaintext: &[u8],
213        protocol: &Protocol,
214        key_id: &str,
215        counterparty: &Counterparty,
216    ) -> Result<Vec<u8>, WalletError> {
217        let effective = self.default_counterparty(counterparty, CounterpartyType::Self_);
218        let sym_key = self
219            .key_deriver
220            .derive_symmetric_key(protocol, key_id, &effective)?;
221        Ok(sym_key.encrypt(plaintext)?)
222    }
223
224    /// Decrypt ciphertext using a derived symmetric key (AES-GCM).
225    ///
226    /// Derives the same symmetric key used for encryption and decrypts.
227    /// Expects format: IV(32) || ciphertext || auth tag(16).
228    pub fn decrypt_sync(
229        &self,
230        ciphertext: &[u8],
231        protocol: &Protocol,
232        key_id: &str,
233        counterparty: &Counterparty,
234    ) -> Result<Vec<u8>, WalletError> {
235        let effective = self.default_counterparty(counterparty, CounterpartyType::Self_);
236        let sym_key = self
237            .key_deriver
238            .derive_symmetric_key(protocol, key_id, &effective)?;
239        Ok(sym_key.decrypt(ciphertext)?)
240    }
241
242    /// Create an HMAC-SHA256 over data using a derived symmetric key.
243    ///
244    /// Returns a 32-byte HMAC value.
245    pub fn create_hmac_sync(
246        &self,
247        data: &[u8],
248        protocol: &Protocol,
249        key_id: &str,
250        counterparty: &Counterparty,
251    ) -> Result<Vec<u8>, WalletError> {
252        let effective = self.default_counterparty(counterparty, CounterpartyType::Self_);
253        let sym_key = self
254            .key_deriver
255            .derive_symmetric_key(protocol, key_id, &effective)?;
256        let key_bytes = sym_key.to_bytes();
257        let hmac = sha256_hmac(&key_bytes, data);
258        Ok(hmac.to_vec())
259    }
260
261    /// Verify an HMAC-SHA256 value over data using a derived symmetric key.
262    ///
263    /// Computes the expected HMAC and compares it with the provided value
264    /// using constant-time comparison.
265    pub fn verify_hmac_sync(
266        &self,
267        data: &[u8],
268        hmac_value: &[u8],
269        protocol: &Protocol,
270        key_id: &str,
271        counterparty: &Counterparty,
272    ) -> Result<bool, WalletError> {
273        let expected = self.create_hmac_sync(data, protocol, key_id, counterparty)?;
274        // Constant-time comparison to prevent timing attacks
275        Ok(constant_time_eq(&expected, hmac_value))
276    }
277
278    /// Reveal counterparty key linkage to a verifier.
279    ///
280    /// Creates an encrypted revelation of the shared secret between this wallet
281    /// and the counterparty, along with an encrypted HMAC proof. Both are
282    /// encrypted for the verifier using the "counterparty linkage revelation" protocol.
283    pub fn reveal_counterparty_key_linkage_sync(
284        &self,
285        counterparty: &Counterparty,
286        verifier: &PublicKey,
287    ) -> Result<RevealCounterpartyResult, WalletError> {
288        // Get the shared secret point as a public key
289        let linkage_point = self.key_deriver.reveal_counterparty_secret(counterparty)?;
290        let linkage_bytes = linkage_point.to_der(); // compressed 33 bytes
291
292        let prover = self.key_deriver.identity_key();
293
294        // Create a revelation timestamp
295        // Use a simple UTC timestamp format
296        let revelation_time = current_utc_timestamp();
297
298        let verifier_counterparty = Counterparty {
299            counterparty_type: CounterpartyType::Other,
300            public_key: Some(verifier.clone()),
301        };
302
303        let linkage_protocol = Protocol {
304            security_level: 2,
305            protocol: "counterparty linkage revelation".to_string(),
306        };
307
308        // Encrypt the linkage bytes for the verifier
309        let encrypted_linkage = self.encrypt_sync(
310            &linkage_bytes,
311            &linkage_protocol,
312            &revelation_time,
313            &verifier_counterparty,
314        )?;
315
316        // Create Schnorr DLEQ proof (matching TS SDK ProtoWallet.ts)
317        // Proves knowledge of private key `a` such that A = a*G and S = a*B
318        let linkage_point = Point::from_der(&linkage_bytes)
319            .map_err(|e| WalletError::Internal(format!("invalid linkage point: {}", e)))?;
320        let counterparty_pub = match &counterparty.public_key {
321            Some(pk) => pk.clone(),
322            None => {
323                return Err(WalletError::InvalidParameter(
324                    "counterparty public key required for linkage revelation".to_string(),
325                ))
326            }
327        };
328        let schnorr_proof = schnorr_generate_proof(
329            self.key_deriver.root_key(),
330            &self.key_deriver.identity_key(),
331            &counterparty_pub,
332            &linkage_point,
333        )?;
334        // Serialize proof as R(33) || S'(33) || z(variable) matching TS SDK format
335        let mut proof_bin = Vec::with_capacity(33 + 33 + 32);
336        proof_bin.extend_from_slice(&schnorr_proof.r_point.to_der(true));
337        proof_bin.extend_from_slice(&schnorr_proof.s_prime.to_der(true));
338        proof_bin.extend_from_slice(&schnorr_proof.z.to_bytes());
339        let encrypted_proof = self.encrypt_sync(
340            &proof_bin,
341            &linkage_protocol,
342            &revelation_time,
343            &verifier_counterparty,
344        )?;
345
346        Ok(RevealCounterpartyResult {
347            prover,
348            counterparty: counterparty_pub,
349            verifier: verifier.clone(),
350            revelation_time,
351            encrypted_linkage,
352            encrypted_linkage_proof: encrypted_proof,
353        })
354    }
355
356    /// Reveal specific key linkage for a given protocol and key ID to a verifier.
357    ///
358    /// Encrypts the specific secret and a proof byte for the verifier using a
359    /// special "specific linkage revelation" protocol.
360    pub fn reveal_specific_key_linkage_sync(
361        &self,
362        counterparty: &Counterparty,
363        verifier: &PublicKey,
364        protocol: &Protocol,
365        key_id: &str,
366    ) -> Result<RevealSpecificResult, WalletError> {
367        // Get the specific secret (HMAC of shared secret + invoice number)
368        let linkage = self
369            .key_deriver
370            .reveal_specific_secret(counterparty, protocol, key_id)?;
371
372        let prover = self.key_deriver.identity_key();
373
374        let verifier_counterparty = Counterparty {
375            counterparty_type: CounterpartyType::Other,
376            public_key: Some(verifier.clone()),
377        };
378
379        // Build the special protocol for specific linkage revelation
380        let encrypt_protocol = Protocol {
381            security_level: 2,
382            protocol: format!(
383                "specific linkage revelation {} {}",
384                protocol.security_level, protocol.protocol
385            ),
386        };
387
388        // Encrypt the linkage for the verifier
389        let encrypted_linkage =
390            self.encrypt_sync(&linkage, &encrypt_protocol, key_id, &verifier_counterparty)?;
391
392        // Encrypt proof type byte (0 = no proof) for the verifier
393        let proof_bytes: [u8; 1] = [0];
394        let encrypted_proof = self.encrypt_sync(
395            &proof_bytes,
396            &encrypt_protocol,
397            key_id,
398            &verifier_counterparty,
399        )?;
400
401        // Extract the counterparty public key
402        let counterparty_pub = match &counterparty.public_key {
403            Some(pk) => pk.clone(),
404            None => {
405                return Err(WalletError::InvalidParameter(
406                    "counterparty public key required for linkage revelation".to_string(),
407                ))
408            }
409        };
410
411        Ok(RevealSpecificResult {
412            encrypted_linkage,
413            encrypted_linkage_proof: encrypted_proof,
414            prover,
415            verifier: verifier.clone(),
416            counterparty: counterparty_pub,
417            protocol: protocol.clone(),
418            key_id: key_id.to_string(),
419            proof_type: 0,
420        })
421    }
422
423    /// Default an Uninitialized counterparty to the given type.
424    fn default_counterparty(
425        &self,
426        counterparty: &Counterparty,
427        default_type: CounterpartyType,
428    ) -> Counterparty {
429        if counterparty.counterparty_type == CounterpartyType::Uninitialized {
430            Counterparty {
431                counterparty_type: default_type,
432                public_key: None,
433            }
434        } else {
435            counterparty.clone()
436        }
437    }
438}
439
440// ---------------------------------------------------------------------------
441// WalletInterface implementation
442// ---------------------------------------------------------------------------
443//
444// Matches TS SDK CompletedProtoWallet: crypto methods delegate to existing
445// ProtoWallet logic; all other methods return NotImplemented.
446
447#[async_trait::async_trait]
448impl WalletInterface for ProtoWallet {
449    // -- Action methods (not supported) --
450
451    async fn create_action(
452        &self,
453        _args: CreateActionArgs,
454        _originator: Option<&str>,
455    ) -> Result<CreateActionResult, WalletError> {
456        Err(WalletError::NotImplemented("createAction".to_string()))
457    }
458
459    async fn sign_action(
460        &self,
461        _args: SignActionArgs,
462        _originator: Option<&str>,
463    ) -> Result<SignActionResult, WalletError> {
464        Err(WalletError::NotImplemented("signAction".to_string()))
465    }
466
467    async fn abort_action(
468        &self,
469        _args: AbortActionArgs,
470        _originator: Option<&str>,
471    ) -> Result<AbortActionResult, WalletError> {
472        Err(WalletError::NotImplemented("abortAction".to_string()))
473    }
474
475    async fn list_actions(
476        &self,
477        _args: ListActionsArgs,
478        _originator: Option<&str>,
479    ) -> Result<ListActionsResult, WalletError> {
480        Err(WalletError::NotImplemented("listActions".to_string()))
481    }
482
483    async fn internalize_action(
484        &self,
485        _args: InternalizeActionArgs,
486        _originator: Option<&str>,
487    ) -> Result<InternalizeActionResult, WalletError> {
488        Err(WalletError::NotImplemented("internalizeAction".to_string()))
489    }
490
491    // -- Output methods (not supported) --
492
493    async fn list_outputs(
494        &self,
495        _args: ListOutputsArgs,
496        _originator: Option<&str>,
497    ) -> Result<ListOutputsResult, WalletError> {
498        Err(WalletError::NotImplemented("listOutputs".to_string()))
499    }
500
501    async fn relinquish_output(
502        &self,
503        _args: RelinquishOutputArgs,
504        _originator: Option<&str>,
505    ) -> Result<RelinquishOutputResult, WalletError> {
506        Err(WalletError::NotImplemented("relinquishOutput".to_string()))
507    }
508
509    // -- Key/Crypto methods (supported — delegates to ProtoWallet methods) --
510
511    async fn get_public_key(
512        &self,
513        args: GetPublicKeyArgs,
514        _originator: Option<&str>,
515    ) -> Result<GetPublicKeyResult, WalletError> {
516        if args.privileged {
517            return Err(WalletError::NotImplemented(
518                "privileged key access not supported by ProtoWallet".to_string(),
519            ));
520        }
521        let protocol = args.protocol_id.unwrap_or(Protocol {
522            security_level: 0,
523            protocol: String::new(),
524        });
525        let key_id = args.key_id.unwrap_or_default();
526        let counterparty = args.counterparty.unwrap_or(Counterparty {
527            counterparty_type: CounterpartyType::Uninitialized,
528            public_key: None,
529        });
530        let for_self = args.for_self.unwrap_or(false);
531        let pk = self.get_public_key_sync(
532            &protocol,
533            &key_id,
534            &counterparty,
535            for_self,
536            args.identity_key,
537        )?;
538        Ok(GetPublicKeyResult { public_key: pk })
539    }
540
541    async fn reveal_counterparty_key_linkage(
542        &self,
543        args: RevealCounterpartyKeyLinkageArgs,
544        _originator: Option<&str>,
545    ) -> Result<RevealCounterpartyKeyLinkageResult, WalletError> {
546        let counterparty = Counterparty {
547            counterparty_type: CounterpartyType::Other,
548            public_key: Some(args.counterparty),
549        };
550        let result = self.reveal_counterparty_key_linkage_sync(&counterparty, &args.verifier)?;
551        Ok(RevealCounterpartyKeyLinkageResult {
552            prover: result.prover,
553            counterparty: result.counterparty,
554            verifier: result.verifier,
555            revelation_time: result.revelation_time,
556            encrypted_linkage: result.encrypted_linkage,
557            encrypted_linkage_proof: result.encrypted_linkage_proof,
558        })
559    }
560
561    async fn reveal_specific_key_linkage(
562        &self,
563        args: RevealSpecificKeyLinkageArgs,
564        _originator: Option<&str>,
565    ) -> Result<RevealSpecificKeyLinkageResult, WalletError> {
566        let result = self.reveal_specific_key_linkage_sync(
567            &args.counterparty,
568            &args.verifier,
569            &args.protocol_id,
570            &args.key_id,
571        )?;
572        Ok(RevealSpecificKeyLinkageResult {
573            encrypted_linkage: result.encrypted_linkage,
574            encrypted_linkage_proof: result.encrypted_linkage_proof,
575            prover: result.prover,
576            verifier: result.verifier,
577            counterparty: result.counterparty,
578            protocol_id: result.protocol.clone(),
579            key_id: result.key_id.clone(),
580            proof_type: result.proof_type,
581        })
582    }
583
584    async fn encrypt(
585        &self,
586        args: EncryptArgs,
587        _originator: Option<&str>,
588    ) -> Result<EncryptResult, WalletError> {
589        let ciphertext = self.encrypt_sync(
590            &args.plaintext,
591            &args.protocol_id,
592            &args.key_id,
593            &args.counterparty,
594        )?;
595        Ok(EncryptResult { ciphertext })
596    }
597
598    async fn decrypt(
599        &self,
600        args: DecryptArgs,
601        _originator: Option<&str>,
602    ) -> Result<DecryptResult, WalletError> {
603        let plaintext = self.decrypt_sync(
604            &args.ciphertext,
605            &args.protocol_id,
606            &args.key_id,
607            &args.counterparty,
608        )?;
609        Ok(DecryptResult { plaintext })
610    }
611
612    async fn create_hmac(
613        &self,
614        args: CreateHmacArgs,
615        _originator: Option<&str>,
616    ) -> Result<CreateHmacResult, WalletError> {
617        let hmac = self.create_hmac_sync(
618            &args.data,
619            &args.protocol_id,
620            &args.key_id,
621            &args.counterparty,
622        )?;
623        Ok(CreateHmacResult { hmac })
624    }
625
626    async fn verify_hmac(
627        &self,
628        args: VerifyHmacArgs,
629        _originator: Option<&str>,
630    ) -> Result<VerifyHmacResult, WalletError> {
631        let valid = self.verify_hmac_sync(
632            &args.data,
633            &args.hmac,
634            &args.protocol_id,
635            &args.key_id,
636            &args.counterparty,
637        )?;
638        // Match TS SDK behavior: throw error on invalid HMAC instead of returning false
639        if !valid {
640            return Err(WalletError::InvalidHmac);
641        }
642        Ok(VerifyHmacResult { valid: true })
643    }
644
645    async fn create_signature(
646        &self,
647        args: CreateSignatureArgs,
648        _originator: Option<&str>,
649    ) -> Result<CreateSignatureResult, WalletError> {
650        let signature = self.create_signature_sync(
651            args.data.as_deref(),
652            args.hash_to_directly_sign.as_deref(),
653            &args.protocol_id,
654            &args.key_id,
655            &args.counterparty,
656        )?;
657        Ok(CreateSignatureResult { signature })
658    }
659
660    async fn verify_signature(
661        &self,
662        args: VerifySignatureArgs,
663        _originator: Option<&str>,
664    ) -> Result<VerifySignatureResult, WalletError> {
665        let for_self = args.for_self.unwrap_or(false);
666        let valid = self.verify_signature_sync(
667            args.data.as_deref(),
668            args.hash_to_directly_verify.as_deref(),
669            &args.signature,
670            &args.protocol_id,
671            &args.key_id,
672            &args.counterparty,
673            for_self,
674        )?;
675        // Match TS SDK behavior: throw error on invalid signature instead of returning false
676        if !valid {
677            return Err(WalletError::InvalidSignature);
678        }
679        Ok(VerifySignatureResult { valid: true })
680    }
681
682    // -- Certificate methods (not supported) --
683
684    async fn acquire_certificate(
685        &self,
686        _args: AcquireCertificateArgs,
687        _originator: Option<&str>,
688    ) -> Result<Certificate, WalletError> {
689        Err(WalletError::NotImplemented(
690            "acquireCertificate".to_string(),
691        ))
692    }
693
694    async fn list_certificates(
695        &self,
696        _args: ListCertificatesArgs,
697        _originator: Option<&str>,
698    ) -> Result<ListCertificatesResult, WalletError> {
699        Err(WalletError::NotImplemented("listCertificates".to_string()))
700    }
701
702    async fn prove_certificate(
703        &self,
704        _args: ProveCertificateArgs,
705        _originator: Option<&str>,
706    ) -> Result<ProveCertificateResult, WalletError> {
707        Err(WalletError::NotImplemented("proveCertificate".to_string()))
708    }
709
710    async fn relinquish_certificate(
711        &self,
712        _args: RelinquishCertificateArgs,
713        _originator: Option<&str>,
714    ) -> Result<RelinquishCertificateResult, WalletError> {
715        Err(WalletError::NotImplemented(
716            "relinquishCertificate".to_string(),
717        ))
718    }
719
720    // -- Discovery methods (not supported) --
721
722    async fn discover_by_identity_key(
723        &self,
724        _args: DiscoverByIdentityKeyArgs,
725        _originator: Option<&str>,
726    ) -> Result<DiscoverCertificatesResult, WalletError> {
727        Err(WalletError::NotImplemented(
728            "discoverByIdentityKey".to_string(),
729        ))
730    }
731
732    async fn discover_by_attributes(
733        &self,
734        _args: DiscoverByAttributesArgs,
735        _originator: Option<&str>,
736    ) -> Result<DiscoverCertificatesResult, WalletError> {
737        Err(WalletError::NotImplemented(
738            "discoverByAttributes".to_string(),
739        ))
740    }
741
742    // -- Auth/Info methods (not supported) --
743
744    async fn is_authenticated(
745        &self,
746        _originator: Option<&str>,
747    ) -> Result<AuthenticatedResult, WalletError> {
748        Err(WalletError::NotImplemented("isAuthenticated".to_string()))
749    }
750
751    async fn wait_for_authentication(
752        &self,
753        _originator: Option<&str>,
754    ) -> Result<AuthenticatedResult, WalletError> {
755        Err(WalletError::NotImplemented(
756            "waitForAuthentication".to_string(),
757        ))
758    }
759
760    async fn get_height(&self, _originator: Option<&str>) -> Result<GetHeightResult, WalletError> {
761        Err(WalletError::NotImplemented("getHeight".to_string()))
762    }
763
764    async fn get_header_for_height(
765        &self,
766        _args: GetHeaderArgs,
767        _originator: Option<&str>,
768    ) -> Result<GetHeaderResult, WalletError> {
769        Err(WalletError::NotImplemented(
770            "getHeaderForHeight".to_string(),
771        ))
772    }
773
774    async fn get_network(
775        &self,
776        _originator: Option<&str>,
777    ) -> Result<GetNetworkResult, WalletError> {
778        Err(WalletError::NotImplemented("getNetwork".to_string()))
779    }
780
781    async fn get_version(
782        &self,
783        _originator: Option<&str>,
784    ) -> Result<GetVersionResult, WalletError> {
785        Err(WalletError::NotImplemented("getVersion".to_string()))
786    }
787}
788
789/// Constant-time byte comparison to prevent timing attacks.
790fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
791    if a.len() != b.len() {
792        return false;
793    }
794    let mut diff: u8 = 0;
795    for (x, y) in a.iter().zip(b.iter()) {
796        diff |= x ^ y;
797    }
798    diff == 0
799}
800
801/// Returns a UTC timestamp string suitable for use as a key ID.
802fn current_utc_timestamp() -> String {
803    // Use a simple epoch-based timestamp to avoid external dependencies.
804    // Format: seconds since epoch as a string.
805    use std::time::{SystemTime, UNIX_EPOCH};
806    let now = SystemTime::now()
807        .duration_since(UNIX_EPOCH)
808        .unwrap_or_default();
809    format!("{}", now.as_secs())
810}
811
812#[cfg(test)]
813mod tests {
814    use super::*;
815    use crate::wallet::types::{BooleanDefaultFalse, BooleanDefaultTrue};
816
817    fn test_protocol() -> Protocol {
818        Protocol {
819            security_level: 2,
820            protocol: "test proto wallet".to_string(),
821        }
822    }
823
824    fn self_counterparty() -> Counterparty {
825        Counterparty {
826            counterparty_type: CounterpartyType::Self_,
827            public_key: None,
828        }
829    }
830
831    fn test_private_key() -> PrivateKey {
832        PrivateKey::from_hex("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890")
833            .unwrap()
834    }
835
836    #[test]
837    fn test_new_creates_wallet_with_correct_identity_key() {
838        let pk = test_private_key();
839        let expected_pub = pk.to_public_key();
840        let wallet = ProtoWallet::new(pk);
841        let identity = wallet
842            .get_public_key_sync(&test_protocol(), "1", &self_counterparty(), false, true)
843            .unwrap();
844        assert_eq!(identity.to_der_hex(), expected_pub.to_der_hex());
845    }
846
847    #[test]
848    fn test_get_public_key_identity_key_true() {
849        let pk = test_private_key();
850        let expected = pk.to_public_key().to_der_hex();
851        let wallet = ProtoWallet::new(pk);
852        let result = wallet
853            .get_public_key_sync(&test_protocol(), "1", &self_counterparty(), false, true)
854            .unwrap();
855        assert_eq!(result.to_der_hex(), expected);
856    }
857
858    #[test]
859    fn test_get_public_key_derived() {
860        let wallet = ProtoWallet::new(test_private_key());
861        let protocol = test_protocol();
862        let pub1 = wallet
863            .get_public_key_sync(&protocol, "key1", &self_counterparty(), true, false)
864            .unwrap();
865        let pub2 = wallet
866            .get_public_key_sync(&protocol, "key2", &self_counterparty(), true, false)
867            .unwrap();
868        // Different key IDs should produce different derived keys
869        assert_ne!(pub1.to_der_hex(), pub2.to_der_hex());
870    }
871
872    #[test]
873    fn test_create_and_verify_signature_roundtrip() {
874        let wallet = ProtoWallet::new(test_private_key());
875        let protocol = test_protocol();
876        let counterparty = self_counterparty();
877        let data = b"hello world signature test";
878
879        let sig = wallet
880            .create_signature_sync(Some(data), None, &protocol, "sig1", &counterparty)
881            .unwrap();
882        assert!(!sig.is_empty());
883
884        let valid = wallet
885            .verify_signature_sync(
886                Some(data),
887                None,
888                &sig,
889                &protocol,
890                "sig1",
891                &counterparty,
892                true,
893            )
894            .unwrap();
895        assert!(valid, "signature should verify");
896    }
897
898    #[test]
899    fn test_verify_signature_rejects_wrong_data() {
900        let wallet = ProtoWallet::new(test_private_key());
901        let protocol = test_protocol();
902        let counterparty = self_counterparty();
903
904        let sig = wallet
905            .create_signature_sync(
906                Some(b"correct data"),
907                None,
908                &protocol,
909                "sig2",
910                &counterparty,
911            )
912            .unwrap();
913        let valid = wallet
914            .verify_signature_sync(
915                Some(b"wrong data"),
916                None,
917                &sig,
918                &protocol,
919                "sig2",
920                &counterparty,
921                true,
922            )
923            .unwrap();
924        assert!(!valid, "signature should not verify for wrong data");
925    }
926
927    #[test]
928    fn test_encrypt_decrypt_roundtrip() {
929        let wallet = ProtoWallet::new(test_private_key());
930        let protocol = test_protocol();
931        let counterparty = self_counterparty();
932        let plaintext = b"secret message for encryption";
933
934        let ciphertext = wallet
935            .encrypt_sync(plaintext, &protocol, "enc1", &counterparty)
936            .unwrap();
937        assert_ne!(ciphertext.as_slice(), plaintext);
938
939        let decrypted = wallet
940            .decrypt_sync(&ciphertext, &protocol, "enc1", &counterparty)
941            .unwrap();
942        assert_eq!(decrypted, plaintext);
943    }
944
945    #[test]
946    fn test_encrypt_decrypt_empty_plaintext() {
947        let wallet = ProtoWallet::new(test_private_key());
948        let protocol = test_protocol();
949        let counterparty = self_counterparty();
950
951        let ciphertext = wallet
952            .encrypt_sync(b"", &protocol, "enc2", &counterparty)
953            .unwrap();
954        let decrypted = wallet
955            .decrypt_sync(&ciphertext, &protocol, "enc2", &counterparty)
956            .unwrap();
957        assert!(decrypted.is_empty());
958    }
959
960    #[test]
961    fn test_create_and_verify_hmac_roundtrip() {
962        let wallet = ProtoWallet::new(test_private_key());
963        let protocol = test_protocol();
964        let counterparty = self_counterparty();
965        let data = b"hmac test data";
966
967        let hmac = wallet
968            .create_hmac_sync(data, &protocol, "hmac1", &counterparty)
969            .unwrap();
970        assert_eq!(hmac.len(), 32);
971
972        let valid = wallet
973            .verify_hmac_sync(data, &hmac, &protocol, "hmac1", &counterparty)
974            .unwrap();
975        assert!(valid, "HMAC should verify");
976    }
977
978    #[test]
979    fn test_verify_hmac_rejects_wrong_data() {
980        let wallet = ProtoWallet::new(test_private_key());
981        let protocol = test_protocol();
982        let counterparty = self_counterparty();
983
984        let hmac = wallet
985            .create_hmac_sync(b"correct", &protocol, "hmac2", &counterparty)
986            .unwrap();
987        let valid = wallet
988            .verify_hmac_sync(b"wrong", &hmac, &protocol, "hmac2", &counterparty)
989            .unwrap();
990        assert!(!valid, "HMAC should not verify for wrong data");
991    }
992
993    #[test]
994    fn test_hmac_deterministic() {
995        let wallet = ProtoWallet::new(test_private_key());
996        let protocol = test_protocol();
997        let counterparty = self_counterparty();
998        let data = b"deterministic hmac";
999
1000        let hmac1 = wallet
1001            .create_hmac_sync(data, &protocol, "hmac3", &counterparty)
1002            .unwrap();
1003        let hmac2 = wallet
1004            .create_hmac_sync(data, &protocol, "hmac3", &counterparty)
1005            .unwrap();
1006        assert_eq!(hmac1, hmac2);
1007    }
1008
1009    #[test]
1010    fn test_anyone_wallet_encrypt_decrypt() {
1011        let anyone = ProtoWallet::anyone();
1012        let other_key = test_private_key();
1013        let other_pub = other_key.to_public_key();
1014
1015        let counterparty = Counterparty {
1016            counterparty_type: CounterpartyType::Other,
1017            public_key: Some(other_pub),
1018        };
1019        let protocol = test_protocol();
1020        let plaintext = b"message from anyone";
1021
1022        let ciphertext = anyone
1023            .encrypt_sync(plaintext, &protocol, "anon1", &counterparty)
1024            .unwrap();
1025        let decrypted = anyone
1026            .decrypt_sync(&ciphertext, &protocol, "anon1", &counterparty)
1027            .unwrap();
1028        assert_eq!(decrypted, plaintext);
1029    }
1030
1031    #[test]
1032    fn test_uninitialized_counterparty_defaults_to_self_for_encrypt() {
1033        let wallet = ProtoWallet::new(test_private_key());
1034        let protocol = test_protocol();
1035        let uninit = Counterparty {
1036            counterparty_type: CounterpartyType::Uninitialized,
1037            public_key: None,
1038        };
1039        let self_cp = self_counterparty();
1040
1041        let ct_uninit = wallet
1042            .encrypt_sync(b"test", &protocol, "def1", &uninit)
1043            .unwrap();
1044        // Both should decrypt with Self_ counterparty
1045        let decrypted = wallet
1046            .decrypt_sync(&ct_uninit, &protocol, "def1", &self_cp)
1047            .unwrap();
1048        assert_eq!(decrypted, b"test");
1049    }
1050
1051    #[test]
1052    fn test_reveal_specific_key_linkage() {
1053        let wallet_a = ProtoWallet::new(test_private_key());
1054        let verifier_key = PrivateKey::from_hex("ff").unwrap();
1055        let verifier_pub = verifier_key.to_public_key();
1056
1057        let counterparty_key = PrivateKey::from_hex("bb").unwrap();
1058        let counterparty_pub = counterparty_key.to_public_key();
1059
1060        let counterparty = Counterparty {
1061            counterparty_type: CounterpartyType::Other,
1062            public_key: Some(counterparty_pub),
1063        };
1064
1065        let protocol = test_protocol();
1066        let result = wallet_a
1067            .reveal_specific_key_linkage_sync(&counterparty, &verifier_pub, &protocol, "link1")
1068            .unwrap();
1069
1070        assert!(!result.encrypted_linkage.is_empty());
1071        assert!(!result.encrypted_linkage_proof.is_empty());
1072        assert_eq!(result.proof_type, 0);
1073        assert_eq!(result.key_id, "link1");
1074    }
1075
1076    #[test]
1077    fn test_reveal_counterparty_key_linkage() {
1078        let wallet = ProtoWallet::new(test_private_key());
1079        let verifier_key = PrivateKey::from_hex("ff").unwrap();
1080        let verifier_pub = verifier_key.to_public_key();
1081
1082        let counterparty_key = PrivateKey::from_hex("cc").unwrap();
1083        let counterparty_pub = counterparty_key.to_public_key();
1084
1085        let counterparty = Counterparty {
1086            counterparty_type: CounterpartyType::Other,
1087            public_key: Some(counterparty_pub.clone()),
1088        };
1089
1090        let result = wallet
1091            .reveal_counterparty_key_linkage_sync(&counterparty, &verifier_pub)
1092            .unwrap();
1093
1094        assert!(!result.encrypted_linkage.is_empty());
1095        assert!(!result.encrypted_linkage_proof.is_empty());
1096        assert_eq!(
1097            result.counterparty.to_der_hex(),
1098            counterparty_pub.to_der_hex()
1099        );
1100        assert_eq!(result.verifier.to_der_hex(), verifier_pub.to_der_hex());
1101        assert!(!result.revelation_time.is_empty());
1102    }
1103
1104    // -----------------------------------------------------------------------
1105    // WalletInterface trait tests
1106    // -----------------------------------------------------------------------
1107
1108    /// Helper: call WalletInterface method through trait to verify dispatch.
1109    async fn get_pub_key_via_trait<W: WalletInterface + ?Sized>(
1110        w: &W,
1111        args: GetPublicKeyArgs,
1112    ) -> Result<GetPublicKeyResult, WalletError> {
1113        w.get_public_key(args, None).await
1114    }
1115
1116    #[tokio::test]
1117    async fn test_wallet_interface_get_public_key_identity() {
1118        let pk = test_private_key();
1119        let expected = pk.to_public_key().to_der_hex();
1120        let wallet = ProtoWallet::new(pk);
1121
1122        let result = get_pub_key_via_trait(
1123            &wallet,
1124            GetPublicKeyArgs {
1125                identity_key: true,
1126                protocol_id: None,
1127                key_id: None,
1128                counterparty: None,
1129                privileged: false,
1130                privileged_reason: None,
1131                for_self: None,
1132                seek_permission: None,
1133            },
1134        )
1135        .await
1136        .unwrap();
1137
1138        assert_eq!(result.public_key.to_der_hex(), expected);
1139    }
1140
1141    #[tokio::test]
1142    async fn test_wallet_interface_get_public_key_derived() {
1143        let wallet = ProtoWallet::new(test_private_key());
1144
1145        let result = get_pub_key_via_trait(
1146            &wallet,
1147            GetPublicKeyArgs {
1148                identity_key: false,
1149                protocol_id: Some(test_protocol()),
1150                key_id: Some("derived1".to_string()),
1151                counterparty: Some(self_counterparty()),
1152                privileged: false,
1153                privileged_reason: None,
1154                for_self: Some(true),
1155                seek_permission: None,
1156            },
1157        )
1158        .await
1159        .unwrap();
1160
1161        // Should match the direct method call
1162        let direct = wallet
1163            .get_public_key_sync(
1164                &test_protocol(),
1165                "derived1",
1166                &self_counterparty(),
1167                true,
1168                false,
1169            )
1170            .unwrap();
1171        assert_eq!(result.public_key.to_der_hex(), direct.to_der_hex());
1172    }
1173
1174    #[tokio::test]
1175    async fn test_wallet_interface_privileged_rejected() {
1176        let wallet = ProtoWallet::new(test_private_key());
1177        let err = WalletInterface::get_public_key(
1178            &wallet,
1179            GetPublicKeyArgs {
1180                identity_key: true,
1181                protocol_id: None,
1182                key_id: None,
1183                counterparty: None,
1184                privileged: true,
1185                privileged_reason: Some("test".to_string()),
1186                for_self: None,
1187                seek_permission: None,
1188            },
1189            None,
1190        )
1191        .await;
1192
1193        assert!(err.is_err());
1194        let msg = format!("{}", err.unwrap_err());
1195        assert!(msg.contains("not implemented"), "got: {}", msg);
1196    }
1197
1198    #[tokio::test]
1199    async fn test_wallet_interface_create_verify_signature() {
1200        let wallet = ProtoWallet::new(test_private_key());
1201        let data = b"test data for wallet interface sig".to_vec();
1202
1203        let sig_result = WalletInterface::create_signature(
1204            &wallet,
1205            CreateSignatureArgs {
1206                protocol_id: test_protocol(),
1207                key_id: "wsig1".to_string(),
1208                counterparty: self_counterparty(),
1209                data: Some(data.clone()),
1210                hash_to_directly_sign: None,
1211                privileged: false,
1212                privileged_reason: None,
1213                seek_permission: None,
1214            },
1215            None,
1216        )
1217        .await
1218        .unwrap();
1219
1220        let verify_result = WalletInterface::verify_signature(
1221            &wallet,
1222            VerifySignatureArgs {
1223                protocol_id: test_protocol(),
1224                key_id: "wsig1".to_string(),
1225                counterparty: self_counterparty(),
1226                data: Some(data),
1227                hash_to_directly_verify: None,
1228                signature: sig_result.signature,
1229                for_self: Some(true),
1230                privileged: false,
1231                privileged_reason: None,
1232                seek_permission: None,
1233            },
1234            None,
1235        )
1236        .await
1237        .unwrap();
1238
1239        assert!(verify_result.valid);
1240    }
1241
1242    #[tokio::test]
1243    async fn test_wallet_interface_encrypt_decrypt() {
1244        let wallet = ProtoWallet::new(test_private_key());
1245        let plaintext = b"wallet interface encrypt test".to_vec();
1246
1247        let enc = WalletInterface::encrypt(
1248            &wallet,
1249            EncryptArgs {
1250                protocol_id: test_protocol(),
1251                key_id: "wenc1".to_string(),
1252                counterparty: self_counterparty(),
1253                plaintext: plaintext.clone(),
1254                privileged: false,
1255                privileged_reason: None,
1256                seek_permission: None,
1257            },
1258            None,
1259        )
1260        .await
1261        .unwrap();
1262
1263        let dec = WalletInterface::decrypt(
1264            &wallet,
1265            DecryptArgs {
1266                protocol_id: test_protocol(),
1267                key_id: "wenc1".to_string(),
1268                counterparty: self_counterparty(),
1269                ciphertext: enc.ciphertext,
1270                privileged: false,
1271                privileged_reason: None,
1272                seek_permission: None,
1273            },
1274            None,
1275        )
1276        .await
1277        .unwrap();
1278
1279        assert_eq!(dec.plaintext, plaintext);
1280    }
1281
1282    #[tokio::test]
1283    async fn test_wallet_interface_hmac_roundtrip() {
1284        let wallet = ProtoWallet::new(test_private_key());
1285        let data = b"wallet interface hmac test".to_vec();
1286
1287        let hmac_result = WalletInterface::create_hmac(
1288            &wallet,
1289            CreateHmacArgs {
1290                protocol_id: test_protocol(),
1291                key_id: "whmac1".to_string(),
1292                counterparty: self_counterparty(),
1293                data: data.clone(),
1294                privileged: false,
1295                privileged_reason: None,
1296                seek_permission: None,
1297            },
1298            None,
1299        )
1300        .await
1301        .unwrap();
1302
1303        assert_eq!(hmac_result.hmac.len(), 32);
1304
1305        let verify = WalletInterface::verify_hmac(
1306            &wallet,
1307            VerifyHmacArgs {
1308                protocol_id: test_protocol(),
1309                key_id: "whmac1".to_string(),
1310                counterparty: self_counterparty(),
1311                data,
1312                hmac: hmac_result.hmac,
1313                privileged: false,
1314                privileged_reason: None,
1315                seek_permission: None,
1316            },
1317            None,
1318        )
1319        .await
1320        .unwrap();
1321
1322        assert!(verify.valid);
1323    }
1324
1325    #[tokio::test]
1326    async fn test_wallet_interface_unsupported_methods_return_not_implemented() {
1327        use crate::wallet::interfaces::*;
1328        let wallet = ProtoWallet::new(test_private_key());
1329
1330        // Each unsupported method should return NotImplemented, matching TS SDK
1331        // CompletedProtoWallet which throws "not implemented" for these.
1332        let err = WalletInterface::is_authenticated(&wallet, None).await;
1333        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1334
1335        let err = WalletInterface::wait_for_authentication(&wallet, None).await;
1336        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1337
1338        let err = WalletInterface::get_network(&wallet, None).await;
1339        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1340
1341        let err = WalletInterface::get_version(&wallet, None).await;
1342        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1343
1344        let err = WalletInterface::get_height(&wallet, None).await;
1345        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1346
1347        let err =
1348            WalletInterface::get_header_for_height(&wallet, GetHeaderArgs { height: 0 }, None)
1349                .await;
1350        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1351
1352        let err = WalletInterface::list_outputs(
1353            &wallet,
1354            ListOutputsArgs {
1355                basket: "test".to_string(),
1356                tags: vec![],
1357                tag_query_mode: None,
1358                include: None,
1359                include_custom_instructions: BooleanDefaultFalse(None),
1360                include_tags: BooleanDefaultFalse(None),
1361                include_labels: BooleanDefaultFalse(None),
1362                limit: Some(10),
1363                offset: None,
1364                seek_permission: BooleanDefaultTrue(None),
1365            },
1366            None,
1367        )
1368        .await;
1369        assert!(matches!(err, Err(WalletError::NotImplemented(_))));
1370    }
1371
1372    #[tokio::test]
1373    async fn test_wallet_interface_reveal_counterparty_key_linkage() {
1374        let wallet = ProtoWallet::new(test_private_key());
1375        let verifier_key = PrivateKey::from_hex("ff").unwrap();
1376        let counterparty_key = PrivateKey::from_hex("cc").unwrap();
1377
1378        let result = WalletInterface::reveal_counterparty_key_linkage(
1379            &wallet,
1380            RevealCounterpartyKeyLinkageArgs {
1381                counterparty: counterparty_key.to_public_key(),
1382                verifier: verifier_key.to_public_key(),
1383                privileged: None,
1384                privileged_reason: None,
1385            },
1386            None,
1387        )
1388        .await
1389        .unwrap();
1390
1391        assert!(!result.encrypted_linkage.is_empty());
1392        assert!(!result.encrypted_linkage_proof.is_empty());
1393        assert_eq!(
1394            result.counterparty.to_der_hex(),
1395            counterparty_key.to_public_key().to_der_hex()
1396        );
1397        assert!(!result.revelation_time.is_empty());
1398    }
1399
1400    #[tokio::test]
1401    async fn test_wallet_interface_reveal_specific_key_linkage() {
1402        let wallet = ProtoWallet::new(test_private_key());
1403        let verifier_key = PrivateKey::from_hex("ff").unwrap();
1404        let counterparty_key = PrivateKey::from_hex("bb").unwrap();
1405
1406        let result = WalletInterface::reveal_specific_key_linkage(
1407            &wallet,
1408            RevealSpecificKeyLinkageArgs {
1409                counterparty: Counterparty {
1410                    counterparty_type: CounterpartyType::Other,
1411                    public_key: Some(counterparty_key.to_public_key()),
1412                },
1413                verifier: verifier_key.to_public_key(),
1414                protocol_id: test_protocol(),
1415                key_id: "wlink1".to_string(),
1416                privileged: None,
1417                privileged_reason: None,
1418            },
1419            None,
1420        )
1421        .await
1422        .unwrap();
1423
1424        assert!(!result.encrypted_linkage.is_empty());
1425        assert_eq!(result.proof_type, 0);
1426        assert_eq!(result.key_id, "wlink1");
1427    }
1428}