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