Skip to main content

bsv_wallet_toolbox/auth_manager/
mod.rs

1//! Authentication manager for WAB-based wallet authentication.
2//!
3//! Provides `WalletAuthenticationManager` which wraps an inner wallet with
4//! WAB-based authentication flow. Before authentication completes, all
5//! WalletInterface methods (except `is_authenticated` and `wait_for_authentication`)
6//! return `WERR_NOT_ACTIVE`. After successful auth, calls delegate to the inner wallet.
7//!
8//! Composes CWI base class logic (UMP tokens, PBKDF2 key derivation, password +
9//! presentation key management) and R-Puzzle faucet funding.
10
11/// CWI-style key derivation functions (PBKDF2, XOR, identity key derivation).
12pub mod cwi_logic;
13/// Authentication state types, snapshot, and profile.
14pub mod types;
15/// UMP token struct and overlay lookup/creation.
16pub mod ump_token;
17
18use std::sync::Arc;
19
20use async_trait::async_trait;
21use tokio::sync::{Mutex, RwLock};
22
23use bsv::primitives::big_number::BigNumber;
24use bsv::primitives::private_key::PrivateKey;
25use bsv::script::templates::r_puzzle::{RPuzzle, RPuzzleType};
26use bsv::wallet::interfaces::{
27    AbortActionArgs, AbortActionResult, AcquireCertificateArgs, AuthenticatedResult, Certificate,
28    CreateActionArgs, CreateActionResult, CreateHmacArgs, CreateHmacResult, CreateSignatureArgs,
29    CreateSignatureResult, DecryptArgs, DecryptResult, DiscoverByAttributesArgs,
30    DiscoverByIdentityKeyArgs, DiscoverCertificatesResult, EncryptArgs, EncryptResult,
31    GetHeaderArgs, GetHeaderResult, GetHeightResult, GetNetworkResult, GetPublicKeyArgs,
32    GetPublicKeyResult, GetVersionResult, InternalizeActionArgs, InternalizeActionResult,
33    ListActionsArgs, ListActionsResult, ListCertificatesArgs, ListCertificatesResult,
34    ListOutputsArgs, ListOutputsResult, ProveCertificateArgs, ProveCertificateResult,
35    RelinquishCertificateArgs, RelinquishCertificateResult, RelinquishOutputArgs,
36    RelinquishOutputResult, RevealCounterpartyKeyLinkageArgs, RevealCounterpartyKeyLinkageResult,
37    RevealSpecificKeyLinkageArgs, RevealSpecificKeyLinkageResult, SignActionArgs, SignActionResult,
38    VerifyHmacArgs, VerifyHmacResult, VerifySignatureArgs, VerifySignatureResult, WalletInterface,
39};
40
41use crate::wab_client::interactors::AuthMethodInteractor;
42use crate::wab_client::types::{FaucetResponse, StartAuthResponse};
43use crate::wab_client::WABClient;
44use crate::WalletError;
45
46use types::{AuthState, StateSnapshot, WalletBuilderFn};
47
48/// Macro to delegate a WalletInterface method to the inner wallet, or return
49/// `WERR_NOT_ACTIVE` if no inner wallet is available (pre-authentication).
50macro_rules! delegate_or_not_active {
51    ($self:ident, $method:ident, $args:expr, $originator:expr) => {{
52        let guard = $self.inner.read().await;
53        match guard.as_ref() {
54            Some(w) => w.$method($args, $originator).await,
55            None => Err(bsv::wallet::error::WalletError::Internal(
56                "WERR_NOT_ACTIVE: Wallet is not authenticated".into(),
57            )),
58        }
59    }};
60    // Variant without args (for get_height, get_network, get_version)
61    ($self:ident, $method:ident, $originator:expr) => {{
62        let guard = $self.inner.read().await;
63        match guard.as_ref() {
64            Some(w) => w.$method($originator).await,
65            None => Err(bsv::wallet::error::WalletError::Internal(
66                "WERR_NOT_ACTIVE: Wallet is not authenticated".into(),
67            )),
68        }
69    }};
70}
71
72/// WalletAuthenticationManager wraps a wallet with WAB-based authentication.
73///
74/// Before authentication completes, all WalletInterface methods except
75/// `is_authenticated` and `wait_for_authentication` return `WERR_NOT_ACTIVE`.
76/// After authentication, all calls are delegated to the inner wallet.
77///
78/// The authentication flow:
79/// 1. Application calls `start_auth()` with an auth method interactor and payload
80/// 2. WAB server initiates verification (e.g., sends SMS code)
81/// 3. Application calls `complete_auth()` with verification payload
82/// 4. Manager determines new vs existing user via UMP token lookup
83/// 5. Root key derived from presentation key + password logic
84/// 6. Inner wallet constructed via `wallet_builder`
85/// 7. For new users, optional faucet funding via R-Puzzle redemption
86/// 8. `wait_for_authentication` resolvers are notified
87pub struct WalletAuthenticationManager {
88    /// The inner wallet; `None` before authentication, `Some` after.
89    inner: RwLock<Option<Arc<dyn WalletInterface + Send + Sync>>>,
90    /// Sender to signal authentication completion.
91    auth_tx: tokio::sync::watch::Sender<bool>,
92    /// Receiver to wait for authentication completion.
93    auth_rx: tokio::sync::watch::Receiver<bool>,
94    /// WAB HTTP client for authentication API calls.
95    wab_client: WABClient,
96    /// Async closure to construct the inner wallet from root key material.
97    wallet_builder: WalletBuilderFn,
98    /// Admin originator domain for privileged operations.
99    admin_originator: String,
100    /// Current authentication state.
101    state: Mutex<AuthState>,
102    /// Generated or restored presentation key (hex).
103    presentation_key: Mutex<Option<String>>,
104}
105
106impl WalletAuthenticationManager {
107    /// Create a new `WalletAuthenticationManager`.
108    ///
109    /// # Arguments
110    /// * `wab_client` - WAB HTTP client for auth API calls
111    /// * `wallet_builder` - Async closure to construct inner wallet from root key
112    /// * `admin_originator` - Admin domain for privileged operations
113    /// * `state_snapshot` - Optional serialized `StateSnapshot` to restore state
114    pub fn new(
115        wab_client: WABClient,
116        wallet_builder: WalletBuilderFn,
117        admin_originator: String,
118        state_snapshot: Option<Vec<u8>>,
119    ) -> Result<Self, WalletError> {
120        let (auth_tx, auth_rx) = tokio::sync::watch::channel(false);
121
122        let (initial_state, initial_key) = if let Some(snapshot_bytes) = state_snapshot {
123            let snapshot: StateSnapshot = serde_json::from_slice(&snapshot_bytes)?;
124            let key = snapshot.presentation_key.clone();
125            (snapshot.auth_state, key)
126        } else {
127            (AuthState::Unauthenticated, None)
128        };
129
130        Ok(Self {
131            inner: RwLock::new(None),
132            auth_tx,
133            auth_rx,
134            wab_client,
135            wallet_builder,
136            admin_originator,
137            state: Mutex::new(initial_state),
138            presentation_key: Mutex::new(initial_key),
139        })
140    }
141
142    // ========================================================================
143    // Auth flow methods (not part of WalletInterface)
144    // ========================================================================
145
146    /// Start the WAB authentication flow.
147    ///
148    /// Generates a temporary presentation key if none is set, then calls
149    /// `start_auth_method` on the WAB client with the chosen interactor's method type.
150    ///
151    /// # Arguments
152    /// * `interactor` - The auth method interactor (e.g., Twilio, Persona)
153    /// * `payload` - Method-specific payload (e.g., phone number for Twilio)
154    pub async fn start_auth(
155        &self,
156        interactor: &dyn AuthMethodInteractor,
157        payload: serde_json::Value,
158    ) -> Result<StartAuthResponse, WalletError> {
159        // Generate presentation key if not already set
160        {
161            let mut pk = self.presentation_key.lock().await;
162            if pk.is_none() {
163                *pk = Some(WABClient::generate_random_presentation_key()?);
164            }
165        }
166
167        let pk = self.presentation_key.lock().await;
168        let presentation_key = pk
169            .as_ref()
170            .ok_or_else(|| WalletError::Internal("Presentation key missing".to_string()))?;
171
172        // Update state to Authenticating
173        {
174            let mut state = self.state.lock().await;
175            *state = AuthState::Authenticating;
176        }
177
178        let method_type = interactor.method_type();
179        self.wab_client
180            .start_auth_method(presentation_key, method_type, payload)
181            .await
182    }
183
184    /// Complete the WAB authentication flow.
185    ///
186    /// On success:
187    /// 1. Retrieves final presentation key from WAB
188    /// 2. Looks up UMP token to determine new vs existing user
189    /// 3. Derives root key from presentation key
190    /// 4. Constructs inner wallet via wallet_builder
191    /// 5. Optionally redeems faucet for new users
192    /// 6. Signals authentication completion
193    ///
194    /// # Arguments
195    /// * `interactor` - The auth method interactor
196    /// * `payload` - Verification payload (e.g., SMS code)
197    pub async fn complete_auth(
198        &self,
199        interactor: &dyn AuthMethodInteractor,
200        payload: serde_json::Value,
201    ) -> Result<(), WalletError> {
202        let temp_key = {
203            let mut pk = self.presentation_key.lock().await;
204            pk.take().ok_or_else(|| {
205                WalletError::Internal("No presentation key; call start_auth first".to_string())
206            })?
207        };
208
209        let method_type = interactor.method_type();
210        let result = self
211            .wab_client
212            .complete_auth_method(&temp_key, method_type, payload)
213            .await?;
214
215        if !result.success {
216            let msg = result
217                .message
218                .unwrap_or_else(|| "Failed to complete WAB auth".to_string());
219            let mut state = self.state.lock().await;
220            *state = AuthState::Failed(msg.clone());
221            return Err(WalletError::Internal(msg));
222        }
223
224        let presentation_key_hex = result.presentation_key.ok_or_else(|| {
225            WalletError::Internal("No presentation key returned from WAB".to_string())
226        })?;
227
228        // Store the final presentation key
229        {
230            let mut pk = self.presentation_key.lock().await;
231            *pk = Some(presentation_key_hex.clone());
232        }
233
234        // Look up UMP token to determine new vs existing user
235        // For now, this always returns None (new user); full overlay integration later
236        let presentation_key_bytes = hex_to_bytes(&presentation_key_hex)?;
237        let _is_new_user = {
238            let guard = self.inner.read().await;
239            if let Some(ref _w) = *guard {
240                // If we somehow already have a wallet, skip lookup
241                false
242            } else {
243                // No wallet yet; use presentation key bytes as root key placeholder
244                true
245            }
246        };
247
248        // Derive root key from presentation key bytes
249        // In the full CWI flow this would combine presentation key with password via UMP token
250        // For now, use presentation key bytes directly as root key material
251        let root_key = if presentation_key_bytes.len() >= 32 {
252            presentation_key_bytes.clone()
253        } else {
254            return Err(WalletError::Internal(
255                "Presentation key too short for root key derivation".to_string(),
256            ));
257        };
258
259        // Build a placeholder PrivilegedKeyManager from root key
260        let priv_key_manager = Arc::new(crate::wallet::privileged::NoOpPrivilegedKeyManager);
261
262        // Construct inner wallet
263        let wallet = (self.wallet_builder)(root_key, priv_key_manager).await?;
264
265        // Store inner wallet
266        {
267            let mut inner = self.inner.write().await;
268            *inner = Some(wallet);
269        }
270
271        // Update state
272        {
273            let mut state = self.state.lock().await;
274            *state = AuthState::Authenticated;
275        }
276
277        // Signal authentication completion
278        let _ = self.auth_tx.send(true);
279
280        Ok(())
281    }
282
283    /// Serialize current state to a JSON byte vector for persistence.
284    ///
285    /// The returned bytes can be passed to `new()` as `state_snapshot` to
286    /// restore manager state across process restarts.
287    pub async fn get_state_snapshot(&self) -> Result<Vec<u8>, WalletError> {
288        let state = self.state.lock().await.clone();
289        let pk = self.presentation_key.lock().await.clone();
290        let snapshot = StateSnapshot {
291            presentation_key: pk,
292            auth_state: state,
293            profile: None,
294            is_new_user: None,
295        };
296        serde_json::to_vec(&snapshot).map_err(|e| {
297            WalletError::Internal(format!("Failed to serialize state snapshot: {}", e))
298        })
299    }
300
301    // ========================================================================
302    // R-Puzzle faucet redemption
303    // ========================================================================
304
305    /// Redeem a faucet UTXO using R-Puzzle.
306    ///
307    /// Ports the TS WalletAuthenticationManager faucet logic:
308    /// 1. Creates an action with the faucet UTXO as input (unlockingScriptLength 108)
309    /// 2. Signs with RPuzzle::from_k() using the k-value from the faucet response
310    /// 3. Completes signing via wallet.sign_action()
311    ///
312    /// # Arguments
313    /// * `wallet` - The inner wallet to use for create_action/sign_action
314    /// * `faucet_response` - Response from WABClient::request_faucet containing tx, txid, and k
315    #[allow(dead_code)]
316    async fn redeem_faucet(
317        &self,
318        wallet: &(dyn WalletInterface + Send + Sync),
319        faucet_response: &FaucetResponse,
320    ) -> Result<(), WalletError> {
321        let k_hex = faucet_response
322            .k
323            .as_ref()
324            .ok_or_else(|| WalletError::Internal("Faucet response missing k value".to_string()))?;
325        let txid = faucet_response
326            .txid
327            .as_ref()
328            .ok_or_else(|| WalletError::Internal("Faucet response missing txid".to_string()))?;
329        let tx_hex = faucet_response
330            .tx
331            .as_ref()
332            .ok_or_else(|| WalletError::Internal("Faucet response missing tx data".to_string()))?;
333
334        // Parse the faucet transaction BEEF
335        let tx_bytes = hex_to_bytes(tx_hex)?;
336
337        // Create action with faucet UTXO as input
338        let create_args = CreateActionArgs {
339            input_beef: Some(tx_bytes),
340            inputs: vec![bsv::wallet::interfaces::CreateActionInput {
341                outpoint: format!("{}.0", txid),
342                unlocking_script_length: Some(108),
343                input_description: "Fund from faucet".to_string(),
344                unlocking_script: None,
345                sequence_number: None,
346            }],
347            description: "Fund wallet".to_string(),
348            outputs: vec![],
349            labels: vec![],
350            lock_time: None,
351            version: None,
352            options: Some(bsv::wallet::interfaces::CreateActionOptions {
353                accept_delayed_broadcast: bsv::wallet::types::BooleanDefaultTrue(Some(false)),
354                ..Default::default()
355            }),
356            reference: None,
357        };
358
359        let create_result = wallet
360            .create_action(create_args, Some(&self.admin_originator))
361            .await
362            .map_err(|e| WalletError::Internal(format!("Faucet create_action failed: {}", e)))?;
363
364        let signable = create_result.signable_transaction.ok_or_else(|| {
365            WalletError::Internal("No signable transaction from faucet create_action".to_string())
366        })?;
367
368        // Parse k-value for RPuzzle
369        let k = BigNumber::from_hex(k_hex)
370            .map_err(|e| WalletError::Internal(format!("Failed to parse faucet k value: {}", e)))?;
371        let random_key = PrivateKey::from_random().map_err(|e| {
372            WalletError::Internal(format!("Failed to generate random key for RPuzzle: {}", e))
373        })?;
374
375        // Create RPuzzle unlocking template with k value
376        // The puzzle type is Raw since faucet uses raw R-value matching
377        let puzzle = RPuzzle::from_k(RPuzzleType::Raw, vec![], k, random_key);
378
379        // Convert signable.tx bytes to hex for Transaction::from_beef
380        let tx_hex_str: String = signable.tx.iter().map(|b| format!("{:02x}", b)).collect();
381
382        // Parse the signable transaction BEEF
383        let tx = bsv::transaction::Transaction::from_beef(&tx_hex_str).map_err(|e| {
384            WalletError::Internal(format!("Failed to parse signable transaction BEEF: {}", e))
385        })?;
386
387        // Get the source output's locking script and satoshis from the transaction's
388        // source_transaction on input 0 (set by the wallet during create_action)
389        let (source_sats, source_script) = tx
390            .inputs
391            .first()
392            .and_then(|inp| {
393                inp.source_transaction.as_ref().map(|stx| {
394                    let vout = inp.source_output_index as usize;
395                    let output = &stx.outputs[vout];
396                    (output.satoshis, output.locking_script.clone())
397                })
398            })
399            .ok_or_else(|| {
400                WalletError::Internal(
401                    "Faucet signable tx missing source transaction on input 0".to_string(),
402                )
403            })?;
404
405        // Generate the sighash preimage for input 0
406        let preimage = tx
407            .sighash_preimage(
408                0,
409                bsv::primitives::transaction_signature::SIGHASH_ALL
410                    | bsv::primitives::transaction_signature::SIGHASH_FORKID,
411                source_sats.unwrap_or(0),
412                &source_script,
413            )
414            .map_err(|e| {
415                WalletError::Internal(format!("Failed to compute sighash preimage: {}", e))
416            })?;
417
418        // Unlock with RPuzzle (produces DER signature + sighash byte)
419        let unlocking_script = puzzle
420            .unlock(&preimage)
421            .map_err(|e| WalletError::Internal(format!("RPuzzle unlock failed: {}", e)))?;
422
423        // Sign the action with the RPuzzle unlocking script (binary bytes)
424        let mut spends = std::collections::HashMap::new();
425        spends.insert(
426            0u32,
427            bsv::wallet::interfaces::SignActionSpend {
428                unlocking_script: unlocking_script.to_binary(),
429                sequence_number: None,
430            },
431        );
432
433        let _sign_result = wallet
434            .sign_action(
435                SignActionArgs {
436                    reference: signable.reference,
437                    spends,
438                    options: None,
439                },
440                Some(&self.admin_originator),
441            )
442            .await
443            .map_err(|e| WalletError::Internal(format!("Faucet sign_action failed: {}", e)))?;
444
445        Ok(())
446    }
447}
448
449/// Convert a hex string to bytes.
450fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, WalletError> {
451    if hex.len() % 2 != 0 {
452        return Err(WalletError::Internal(format!(
453            "Invalid hex string length: {}",
454            hex.len()
455        )));
456    }
457    (0..hex.len())
458        .step_by(2)
459        .map(|i| {
460            u8::from_str_radix(&hex[i..i + 2], 16)
461                .map_err(|e| WalletError::Internal(format!("Invalid hex at position {}: {}", i, e)))
462        })
463        .collect()
464}
465
466// ============================================================================
467// WalletInterface implementation
468// ============================================================================
469
470#[async_trait]
471impl WalletInterface for WalletAuthenticationManager {
472    async fn is_authenticated(
473        &self,
474        _originator: Option<&str>,
475    ) -> Result<AuthenticatedResult, bsv::wallet::error::WalletError> {
476        let guard = self.inner.read().await;
477        Ok(AuthenticatedResult {
478            authenticated: guard.is_some(),
479        })
480    }
481
482    async fn wait_for_authentication(
483        &self,
484        _originator: Option<&str>,
485    ) -> Result<AuthenticatedResult, bsv::wallet::error::WalletError> {
486        let mut rx = self.auth_rx.clone();
487        // If already authenticated, return immediately
488        if *rx.borrow() {
489            return Ok(AuthenticatedResult {
490                authenticated: true,
491            });
492        }
493        // Wait for auth signal
494        while rx.changed().await.is_ok() {
495            if *rx.borrow() {
496                return Ok(AuthenticatedResult {
497                    authenticated: true,
498                });
499            }
500        }
501        // Channel closed without auth -- should not happen in normal flow
502        Ok(AuthenticatedResult {
503            authenticated: false,
504        })
505    }
506
507    async fn get_height(
508        &self,
509        originator: Option<&str>,
510    ) -> Result<GetHeightResult, bsv::wallet::error::WalletError> {
511        delegate_or_not_active!(self, get_height, originator)
512    }
513
514    async fn get_header_for_height(
515        &self,
516        args: GetHeaderArgs,
517        originator: Option<&str>,
518    ) -> Result<GetHeaderResult, bsv::wallet::error::WalletError> {
519        delegate_or_not_active!(self, get_header_for_height, args, originator)
520    }
521
522    async fn get_network(
523        &self,
524        originator: Option<&str>,
525    ) -> Result<GetNetworkResult, bsv::wallet::error::WalletError> {
526        delegate_or_not_active!(self, get_network, originator)
527    }
528
529    async fn get_version(
530        &self,
531        originator: Option<&str>,
532    ) -> Result<GetVersionResult, bsv::wallet::error::WalletError> {
533        delegate_or_not_active!(self, get_version, originator)
534    }
535
536    async fn create_action(
537        &self,
538        args: CreateActionArgs,
539        originator: Option<&str>,
540    ) -> Result<CreateActionResult, bsv::wallet::error::WalletError> {
541        delegate_or_not_active!(self, create_action, args, originator)
542    }
543
544    async fn sign_action(
545        &self,
546        args: SignActionArgs,
547        originator: Option<&str>,
548    ) -> Result<SignActionResult, bsv::wallet::error::WalletError> {
549        delegate_or_not_active!(self, sign_action, args, originator)
550    }
551
552    async fn abort_action(
553        &self,
554        args: AbortActionArgs,
555        originator: Option<&str>,
556    ) -> Result<AbortActionResult, bsv::wallet::error::WalletError> {
557        delegate_or_not_active!(self, abort_action, args, originator)
558    }
559
560    async fn list_actions(
561        &self,
562        args: ListActionsArgs,
563        originator: Option<&str>,
564    ) -> Result<ListActionsResult, bsv::wallet::error::WalletError> {
565        delegate_or_not_active!(self, list_actions, args, originator)
566    }
567
568    async fn internalize_action(
569        &self,
570        args: InternalizeActionArgs,
571        originator: Option<&str>,
572    ) -> Result<InternalizeActionResult, bsv::wallet::error::WalletError> {
573        delegate_or_not_active!(self, internalize_action, args, originator)
574    }
575
576    async fn list_outputs(
577        &self,
578        args: ListOutputsArgs,
579        originator: Option<&str>,
580    ) -> Result<ListOutputsResult, bsv::wallet::error::WalletError> {
581        delegate_or_not_active!(self, list_outputs, args, originator)
582    }
583
584    async fn relinquish_output(
585        &self,
586        args: RelinquishOutputArgs,
587        originator: Option<&str>,
588    ) -> Result<RelinquishOutputResult, bsv::wallet::error::WalletError> {
589        delegate_or_not_active!(self, relinquish_output, args, originator)
590    }
591
592    async fn get_public_key(
593        &self,
594        args: GetPublicKeyArgs,
595        originator: Option<&str>,
596    ) -> Result<GetPublicKeyResult, bsv::wallet::error::WalletError> {
597        delegate_or_not_active!(self, get_public_key, args, originator)
598    }
599
600    async fn reveal_counterparty_key_linkage(
601        &self,
602        args: RevealCounterpartyKeyLinkageArgs,
603        originator: Option<&str>,
604    ) -> Result<RevealCounterpartyKeyLinkageResult, bsv::wallet::error::WalletError> {
605        delegate_or_not_active!(self, reveal_counterparty_key_linkage, args, originator)
606    }
607
608    async fn reveal_specific_key_linkage(
609        &self,
610        args: RevealSpecificKeyLinkageArgs,
611        originator: Option<&str>,
612    ) -> Result<RevealSpecificKeyLinkageResult, bsv::wallet::error::WalletError> {
613        delegate_or_not_active!(self, reveal_specific_key_linkage, args, originator)
614    }
615
616    async fn encrypt(
617        &self,
618        args: EncryptArgs,
619        originator: Option<&str>,
620    ) -> Result<EncryptResult, bsv::wallet::error::WalletError> {
621        delegate_or_not_active!(self, encrypt, args, originator)
622    }
623
624    async fn decrypt(
625        &self,
626        args: DecryptArgs,
627        originator: Option<&str>,
628    ) -> Result<DecryptResult, bsv::wallet::error::WalletError> {
629        delegate_or_not_active!(self, decrypt, args, originator)
630    }
631
632    async fn create_hmac(
633        &self,
634        args: CreateHmacArgs,
635        originator: Option<&str>,
636    ) -> Result<CreateHmacResult, bsv::wallet::error::WalletError> {
637        delegate_or_not_active!(self, create_hmac, args, originator)
638    }
639
640    async fn verify_hmac(
641        &self,
642        args: VerifyHmacArgs,
643        originator: Option<&str>,
644    ) -> Result<VerifyHmacResult, bsv::wallet::error::WalletError> {
645        delegate_or_not_active!(self, verify_hmac, args, originator)
646    }
647
648    async fn create_signature(
649        &self,
650        args: CreateSignatureArgs,
651        originator: Option<&str>,
652    ) -> Result<CreateSignatureResult, bsv::wallet::error::WalletError> {
653        delegate_or_not_active!(self, create_signature, args, originator)
654    }
655
656    async fn verify_signature(
657        &self,
658        args: VerifySignatureArgs,
659        originator: Option<&str>,
660    ) -> Result<VerifySignatureResult, bsv::wallet::error::WalletError> {
661        delegate_or_not_active!(self, verify_signature, args, originator)
662    }
663
664    async fn acquire_certificate(
665        &self,
666        args: AcquireCertificateArgs,
667        originator: Option<&str>,
668    ) -> Result<Certificate, bsv::wallet::error::WalletError> {
669        delegate_or_not_active!(self, acquire_certificate, args, originator)
670    }
671
672    async fn list_certificates(
673        &self,
674        args: ListCertificatesArgs,
675        originator: Option<&str>,
676    ) -> Result<ListCertificatesResult, bsv::wallet::error::WalletError> {
677        delegate_or_not_active!(self, list_certificates, args, originator)
678    }
679
680    async fn prove_certificate(
681        &self,
682        args: ProveCertificateArgs,
683        originator: Option<&str>,
684    ) -> Result<ProveCertificateResult, bsv::wallet::error::WalletError> {
685        delegate_or_not_active!(self, prove_certificate, args, originator)
686    }
687
688    async fn relinquish_certificate(
689        &self,
690        args: RelinquishCertificateArgs,
691        originator: Option<&str>,
692    ) -> Result<RelinquishCertificateResult, bsv::wallet::error::WalletError> {
693        delegate_or_not_active!(self, relinquish_certificate, args, originator)
694    }
695
696    async fn discover_by_identity_key(
697        &self,
698        args: DiscoverByIdentityKeyArgs,
699        originator: Option<&str>,
700    ) -> Result<DiscoverCertificatesResult, bsv::wallet::error::WalletError> {
701        delegate_or_not_active!(self, discover_by_identity_key, args, originator)
702    }
703
704    async fn discover_by_attributes(
705        &self,
706        args: DiscoverByAttributesArgs,
707        originator: Option<&str>,
708    ) -> Result<DiscoverCertificatesResult, bsv::wallet::error::WalletError> {
709        delegate_or_not_active!(self, discover_by_attributes, args, originator)
710    }
711}
712
713#[cfg(test)]
714mod tests {
715    use super::*;
716
717    /// Simple mock wallet for auth manager testing.
718    /// `is_authenticated` returns true; all other methods return NotImplemented.
719    struct MockWallet;
720
721    #[async_trait]
722    impl WalletInterface for MockWallet {
723        async fn is_authenticated(
724            &self,
725            _originator: Option<&str>,
726        ) -> Result<AuthenticatedResult, bsv::wallet::error::WalletError> {
727            Ok(AuthenticatedResult {
728                authenticated: true,
729            })
730        }
731
732        async fn wait_for_authentication(
733            &self,
734            _originator: Option<&str>,
735        ) -> Result<AuthenticatedResult, bsv::wallet::error::WalletError> {
736            Ok(AuthenticatedResult {
737                authenticated: true,
738            })
739        }
740
741        async fn get_height(
742            &self,
743            _o: Option<&str>,
744        ) -> Result<GetHeightResult, bsv::wallet::error::WalletError> {
745            Err(bsv::wallet::error::WalletError::NotImplemented(
746                "mock".into(),
747            ))
748        }
749        async fn get_header_for_height(
750            &self,
751            _a: GetHeaderArgs,
752            _o: Option<&str>,
753        ) -> Result<GetHeaderResult, bsv::wallet::error::WalletError> {
754            Err(bsv::wallet::error::WalletError::NotImplemented(
755                "mock".into(),
756            ))
757        }
758        async fn get_network(
759            &self,
760            _o: Option<&str>,
761        ) -> Result<GetNetworkResult, bsv::wallet::error::WalletError> {
762            Err(bsv::wallet::error::WalletError::NotImplemented(
763                "mock".into(),
764            ))
765        }
766        async fn get_version(
767            &self,
768            _o: Option<&str>,
769        ) -> Result<GetVersionResult, bsv::wallet::error::WalletError> {
770            Err(bsv::wallet::error::WalletError::NotImplemented(
771                "mock".into(),
772            ))
773        }
774        async fn create_action(
775            &self,
776            _a: CreateActionArgs,
777            _o: Option<&str>,
778        ) -> Result<CreateActionResult, bsv::wallet::error::WalletError> {
779            Err(bsv::wallet::error::WalletError::NotImplemented(
780                "mock".into(),
781            ))
782        }
783        async fn sign_action(
784            &self,
785            _a: SignActionArgs,
786            _o: Option<&str>,
787        ) -> Result<SignActionResult, bsv::wallet::error::WalletError> {
788            Err(bsv::wallet::error::WalletError::NotImplemented(
789                "mock".into(),
790            ))
791        }
792        async fn abort_action(
793            &self,
794            _a: AbortActionArgs,
795            _o: Option<&str>,
796        ) -> Result<AbortActionResult, bsv::wallet::error::WalletError> {
797            Err(bsv::wallet::error::WalletError::NotImplemented(
798                "mock".into(),
799            ))
800        }
801        async fn list_actions(
802            &self,
803            _a: ListActionsArgs,
804            _o: Option<&str>,
805        ) -> Result<ListActionsResult, bsv::wallet::error::WalletError> {
806            Err(bsv::wallet::error::WalletError::NotImplemented(
807                "mock".into(),
808            ))
809        }
810        async fn internalize_action(
811            &self,
812            _a: InternalizeActionArgs,
813            _o: Option<&str>,
814        ) -> Result<InternalizeActionResult, bsv::wallet::error::WalletError> {
815            Err(bsv::wallet::error::WalletError::NotImplemented(
816                "mock".into(),
817            ))
818        }
819        async fn list_outputs(
820            &self,
821            _a: ListOutputsArgs,
822            _o: Option<&str>,
823        ) -> Result<ListOutputsResult, bsv::wallet::error::WalletError> {
824            Err(bsv::wallet::error::WalletError::NotImplemented(
825                "mock".into(),
826            ))
827        }
828        async fn relinquish_output(
829            &self,
830            _a: RelinquishOutputArgs,
831            _o: Option<&str>,
832        ) -> Result<RelinquishOutputResult, bsv::wallet::error::WalletError> {
833            Err(bsv::wallet::error::WalletError::NotImplemented(
834                "mock".into(),
835            ))
836        }
837        async fn get_public_key(
838            &self,
839            _a: GetPublicKeyArgs,
840            _o: Option<&str>,
841        ) -> Result<GetPublicKeyResult, bsv::wallet::error::WalletError> {
842            Err(bsv::wallet::error::WalletError::NotImplemented(
843                "mock".into(),
844            ))
845        }
846        async fn reveal_counterparty_key_linkage(
847            &self,
848            _a: RevealCounterpartyKeyLinkageArgs,
849            _o: Option<&str>,
850        ) -> Result<RevealCounterpartyKeyLinkageResult, bsv::wallet::error::WalletError> {
851            Err(bsv::wallet::error::WalletError::NotImplemented(
852                "mock".into(),
853            ))
854        }
855        async fn reveal_specific_key_linkage(
856            &self,
857            _a: RevealSpecificKeyLinkageArgs,
858            _o: Option<&str>,
859        ) -> Result<RevealSpecificKeyLinkageResult, bsv::wallet::error::WalletError> {
860            Err(bsv::wallet::error::WalletError::NotImplemented(
861                "mock".into(),
862            ))
863        }
864        async fn encrypt(
865            &self,
866            _a: EncryptArgs,
867            _o: Option<&str>,
868        ) -> Result<EncryptResult, bsv::wallet::error::WalletError> {
869            Err(bsv::wallet::error::WalletError::NotImplemented(
870                "mock".into(),
871            ))
872        }
873        async fn decrypt(
874            &self,
875            _a: DecryptArgs,
876            _o: Option<&str>,
877        ) -> Result<DecryptResult, bsv::wallet::error::WalletError> {
878            Err(bsv::wallet::error::WalletError::NotImplemented(
879                "mock".into(),
880            ))
881        }
882        async fn create_hmac(
883            &self,
884            _a: CreateHmacArgs,
885            _o: Option<&str>,
886        ) -> Result<CreateHmacResult, bsv::wallet::error::WalletError> {
887            Err(bsv::wallet::error::WalletError::NotImplemented(
888                "mock".into(),
889            ))
890        }
891        async fn verify_hmac(
892            &self,
893            _a: VerifyHmacArgs,
894            _o: Option<&str>,
895        ) -> Result<VerifyHmacResult, bsv::wallet::error::WalletError> {
896            Err(bsv::wallet::error::WalletError::NotImplemented(
897                "mock".into(),
898            ))
899        }
900        async fn create_signature(
901            &self,
902            _a: CreateSignatureArgs,
903            _o: Option<&str>,
904        ) -> Result<CreateSignatureResult, bsv::wallet::error::WalletError> {
905            Err(bsv::wallet::error::WalletError::NotImplemented(
906                "mock".into(),
907            ))
908        }
909        async fn verify_signature(
910            &self,
911            _a: VerifySignatureArgs,
912            _o: Option<&str>,
913        ) -> Result<VerifySignatureResult, bsv::wallet::error::WalletError> {
914            Err(bsv::wallet::error::WalletError::NotImplemented(
915                "mock".into(),
916            ))
917        }
918        async fn acquire_certificate(
919            &self,
920            _a: AcquireCertificateArgs,
921            _o: Option<&str>,
922        ) -> Result<Certificate, bsv::wallet::error::WalletError> {
923            Err(bsv::wallet::error::WalletError::NotImplemented(
924                "mock".into(),
925            ))
926        }
927        async fn list_certificates(
928            &self,
929            _a: ListCertificatesArgs,
930            _o: Option<&str>,
931        ) -> Result<ListCertificatesResult, bsv::wallet::error::WalletError> {
932            Err(bsv::wallet::error::WalletError::NotImplemented(
933                "mock".into(),
934            ))
935        }
936        async fn prove_certificate(
937            &self,
938            _a: ProveCertificateArgs,
939            _o: Option<&str>,
940        ) -> Result<ProveCertificateResult, bsv::wallet::error::WalletError> {
941            Err(bsv::wallet::error::WalletError::NotImplemented(
942                "mock".into(),
943            ))
944        }
945        async fn relinquish_certificate(
946            &self,
947            _a: RelinquishCertificateArgs,
948            _o: Option<&str>,
949        ) -> Result<RelinquishCertificateResult, bsv::wallet::error::WalletError> {
950            Err(bsv::wallet::error::WalletError::NotImplemented(
951                "mock".into(),
952            ))
953        }
954        async fn discover_by_identity_key(
955            &self,
956            _a: DiscoverByIdentityKeyArgs,
957            _o: Option<&str>,
958        ) -> Result<DiscoverCertificatesResult, bsv::wallet::error::WalletError> {
959            Err(bsv::wallet::error::WalletError::NotImplemented(
960                "mock".into(),
961            ))
962        }
963        async fn discover_by_attributes(
964            &self,
965            _a: DiscoverByAttributesArgs,
966            _o: Option<&str>,
967        ) -> Result<DiscoverCertificatesResult, bsv::wallet::error::WalletError> {
968            Err(bsv::wallet::error::WalletError::NotImplemented(
969                "mock".into(),
970            ))
971        }
972    }
973
974    fn make_builder() -> WalletBuilderFn {
975        Box::new(|_root_key, _pkm| {
976            Box::pin(async move {
977                let mock: Arc<dyn WalletInterface + Send + Sync> = Arc::new(MockWallet);
978                Ok(mock)
979            })
980        })
981    }
982
983    #[tokio::test]
984    async fn test_auth_manager_not_active_before_auth() {
985        let wab = WABClient::new("http://localhost:3000");
986        let mgr = WalletAuthenticationManager::new(
987            wab,
988            make_builder(),
989            "admin.example.com".to_string(),
990            None,
991        )
992        .unwrap();
993
994        // create_action should return NotActive before authentication
995        let result = mgr
996            .create_action(
997                CreateActionArgs {
998                    description: "test action".to_string(),
999                    inputs: vec![],
1000                    outputs: vec![],
1001                    labels: vec![],
1002                    lock_time: None,
1003                    version: None,
1004                    options: None,
1005                    input_beef: None,
1006                    reference: None,
1007                },
1008                Some("test.com"),
1009            )
1010            .await;
1011
1012        assert!(result.is_err());
1013        let err = result.unwrap_err();
1014        let err_str = err.to_string();
1015        assert!(
1016            err_str.contains("not authenticated") || err_str.contains("NOT_ACTIVE"),
1017            "Expected NOT_ACTIVE error, got: {}",
1018            err_str
1019        );
1020    }
1021
1022    #[tokio::test]
1023    async fn test_is_authenticated_before_and_after() {
1024        let wab = WABClient::new("http://localhost:3000");
1025        let mgr = WalletAuthenticationManager::new(
1026            wab,
1027            make_builder(),
1028            "admin.example.com".to_string(),
1029            None,
1030        )
1031        .unwrap();
1032
1033        // Before auth
1034        let auth = mgr.is_authenticated(None).await.unwrap();
1035        assert!(!auth.authenticated, "Should not be authenticated initially");
1036
1037        // Manually set inner wallet to simulate auth completion
1038        {
1039            let mock: Arc<dyn WalletInterface + Send + Sync> = Arc::new(MockWallet);
1040            let mut inner = mgr.inner.write().await;
1041            *inner = Some(mock);
1042        }
1043
1044        // After auth
1045        let auth = mgr.is_authenticated(None).await.unwrap();
1046        assert!(
1047            auth.authenticated,
1048            "Should be authenticated after setting inner"
1049        );
1050    }
1051}