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