Skip to main content

bsv_wallet_cli/server/
handlers.rs

1use anyhow::{Context, Result};
2use axum::extract::State;
3use axum::http::{HeaderMap, StatusCode};
4use axum::response::{IntoResponse, Response};
5use axum::Json;
6use bsv_sdk::primitives::{to_hex, PublicKey};
7use bsv_sdk::wallet::{
8    // Batch 4: Transaction types (have serde — pass through)
9    AbortActionArgs,
10    AbortActionResult,
11    // Batch 5: Certificate types (have serde — pass through)
12    AcquireCertificateArgs,
13    // Batch 1: Status result types
14    AuthenticatedResult,
15    // Existing endpoint types
16    BasketInsertion,
17    // Core types
18    Counterparty,
19    CreateActionArgs,
20    CreateActionOptions,
21    CreateActionOutput,
22    CreateHmacArgs,
23    CreateSignatureArgs,
24    // Batch 3: Crypto arg types (no serde — manual construction)
25    DecryptArgs,
26    // Batch 6: Discovery types (have serde — pass through)
27    DiscoverByAttributesArgs,
28    DiscoverByIdentityKeyArgs,
29    DiscoverCertificatesResult,
30    EncryptArgs,
31    // Batch 2: Header types
32    GetHeaderArgs,
33    GetHeaderResult,
34    GetHeightResult,
35    GetNetworkResult,
36    GetPublicKeyArgs,
37    GetVersionResult,
38    InternalizeActionArgs,
39    InternalizeOutput,
40    ListActionsArgs,
41    ListActionsResult,
42    ListCertificatesArgs,
43    ListCertificatesResult,
44    // Output types
45    ListOutputsArgs,
46    ListOutputsResult,
47    Protocol,
48    ProveCertificateArgs,
49    ProveCertificateResult,
50    RelinquishCertificateArgs,
51    RelinquishCertificateResult,
52    RelinquishOutputArgs,
53    RelinquishOutputResult,
54    // Key linkage types
55    RevealCounterpartyKeyLinkageResult,
56    RevealSpecificKeyLinkageResult,
57    SecurityLevel,
58    SignActionArgs,
59    SignActionResult,
60    VerifyHmacArgs,
61    VerifySignatureArgs,
62    WalletCertificate,
63    WalletInterface,
64    WalletPayment,
65    WalletRevealCounterpartyArgs,
66    WalletRevealSpecificArgs,
67};
68use bsv_wallet_toolbox::{Services, StorageSqlx, Wallet};
69use serde_json::json;
70use std::sync::Arc;
71
72use super::types::*;
73
74pub type WalletState = Arc<Wallet<StorageSqlx, Services>>;
75
76/// Extract originator from Origin or Originator header
77fn extract_originator(
78    headers: &HeaderMap,
79) -> Result<String, (StatusCode, Json<serde_json::Value>)> {
80    if let Some(origin) = headers.get("origin").and_then(|v| v.to_str().ok()) {
81        if let Ok(url) = url::Url::parse(origin) {
82            if let Some(host) = url.host_str() {
83                return Ok(host.to_string());
84            }
85        }
86    }
87    if let Some(originator) = headers.get("originator").and_then(|v| v.to_str().ok()) {
88        return Ok(originator.to_string());
89    }
90    Err((
91        StatusCode::BAD_REQUEST,
92        Json(json!({"message": "Origin header required"})),
93    ))
94}
95
96/// Classified error response with proper HTTP status codes.
97pub struct AppError {
98    status: StatusCode,
99    code: &'static str,
100    message: String,
101}
102
103impl AppError {
104    /// Classify a wallet error message into an appropriate HTTP status + code.
105    fn classify(msg: &str) -> (StatusCode, &'static str) {
106        // Strip "wallet error: " prefix the SDK wraps errors with
107        let clean = msg.strip_prefix("wallet error: ").unwrap_or(msg);
108
109        if clean.starts_with("Insufficient funds:") || clean.starts_with("insufficient funds:") {
110            (StatusCode::PAYMENT_REQUIRED, "INSUFFICIENT_FUNDS")
111        } else if clean.starts_with("Authentication required")
112            || clean.starts_with("Invalid identity key:")
113        {
114            (StatusCode::UNAUTHORIZED, "AUTHENTICATION_REQUIRED")
115        } else if clean.starts_with("Access denied:") {
116            (StatusCode::FORBIDDEN, "ACCESS_DENIED")
117        } else if clean.starts_with("Entity not found:") || clean.starts_with("not found:") {
118            (StatusCode::NOT_FOUND, "NOT_FOUND")
119        } else if clean.starts_with("Duplicate entity:") || clean.starts_with("Invalid operation:")
120        {
121            (StatusCode::CONFLICT, "CONFLICT")
122        } else if clean.starts_with("Validation error:")
123            || clean.starts_with("Invalid argument:")
124            || clean.starts_with("Origin header required")
125        {
126            (StatusCode::BAD_REQUEST, "VALIDATION_ERROR")
127        } else if clean.starts_with("Storage error:")
128            || clean.starts_with("Database error:")
129            || clean.starts_with("Internal error:")
130        {
131            (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR")
132        } else if clean.starts_with("Service error:")
133            || clean.starts_with("Network error:")
134            || clean.starts_with("Broadcast failed:")
135        {
136            (StatusCode::BAD_GATEWAY, "SERVICE_ERROR")
137        } else {
138            (StatusCode::BAD_REQUEST, "ERROR")
139        }
140    }
141
142    /// Create from a wallet/SDK error (the most common path).
143    fn from_wallet_error(e: impl std::fmt::Display) -> Self {
144        let message = e.to_string();
145        let (status, code) = Self::classify(&message);
146        Self {
147            status,
148            code,
149            message,
150        }
151    }
152}
153
154impl From<anyhow::Error> for AppError {
155    fn from(err: anyhow::Error) -> Self {
156        Self::from_wallet_error(err)
157    }
158}
159
160impl From<(StatusCode, Json<serde_json::Value>)> for AppError {
161    fn from((status, json): (StatusCode, Json<serde_json::Value>)) -> Self {
162        let message = json
163            .0
164            .get("message")
165            .and_then(|m| m.as_str())
166            .unwrap_or_default()
167            .to_string();
168        let (_, code) = Self::classify(&message);
169        Self {
170            status,
171            code,
172            message,
173        }
174    }
175}
176
177impl IntoResponse for AppError {
178    fn into_response(self) -> Response {
179        tracing::warn!(
180            status = self.status.as_u16(),
181            code = self.code,
182            message = %self.message,
183            "request error"
184        );
185        (
186            self.status,
187            Json(json!({"code": self.code, "message": self.message})),
188        )
189            .into_response()
190    }
191}
192
193/// GET /isAuthenticated
194pub async fn is_authenticated() -> Json<serde_json::Value> {
195    Json(json!({"authenticated": true}))
196}
197
198/// POST /getPublicKey
199pub async fn get_public_key(
200    State(wallet): State<WalletState>,
201    headers: HeaderMap,
202    Json(req): Json<McGetPublicKeyReq>,
203) -> Result<Json<McGetPublicKeyRes>, AppError> {
204    let originator = extract_originator(&headers)?;
205
206    let args = GetPublicKeyArgs {
207        identity_key: req.identity_key,
208        protocol_id: req.protocol_id.map(|v| parse_protocol_id(&v)).transpose()?,
209        key_id: req.key_id,
210        counterparty: req
211            .counterparty
212            .map(|s| parse_counterparty(&s))
213            .transpose()?,
214        for_self: req.for_self,
215    };
216
217    let result = wallet
218        .get_public_key(args, &originator)
219        .await
220        .map_err(AppError::from_wallet_error)?;
221
222    Ok(Json(McGetPublicKeyRes {
223        public_key: result.public_key,
224    }))
225}
226
227/// POST /createSignature
228pub async fn create_signature(
229    State(wallet): State<WalletState>,
230    headers: HeaderMap,
231    Json(req): Json<McCreateSignatureReq>,
232) -> Result<Json<McCreateSignatureRes>, AppError> {
233    let originator = extract_originator(&headers)?;
234
235    let args = CreateSignatureArgs {
236        data: Some(req.data),
237        hash_to_directly_sign: None,
238        protocol_id: parse_protocol_id(&req.protocol_id)?,
239        key_id: req.key_id,
240        counterparty: Some(parse_counterparty(&req.counterparty)?),
241    };
242
243    let result = wallet
244        .create_signature(args, &originator)
245        .await
246        .map_err(AppError::from_wallet_error)?;
247
248    Ok(Json(McCreateSignatureRes {
249        signature: result.signature,
250    }))
251}
252
253/// POST /createAction
254///
255/// Spending operations are serialized via `SpendingLock` so that concurrent
256/// requests queue up ("bus stop" pattern) rather than racing on SQLite's write
257/// lock. Non-spending endpoints remain fully concurrent.
258pub async fn create_action(
259    State(wallet): State<WalletState>,
260    axum::Extension(spending_lock): axum::Extension<super::SpendingLock>,
261    headers: HeaderMap,
262    Json(req): Json<McCreateActionReq>,
263) -> Result<Json<McCreateActionRes>, AppError> {
264    let originator = extract_originator(&headers)?;
265
266    let outputs = req
267        .outputs
268        .map(|outs| {
269            outs.into_iter()
270                .map(|o| {
271                    Ok(CreateActionOutput {
272                        locking_script: hex::decode(&o.locking_script)?,
273                        satoshis: o.satoshis,
274                        output_description: o.output_description,
275                        basket: o.basket,
276                        custom_instructions: o.custom_instructions,
277                        tags: o.tags,
278                    })
279                })
280                .collect::<Result<Vec<_>>>()
281        })
282        .transpose()?;
283
284    let args = CreateActionArgs {
285        description: req.description,
286        input_beef: None,
287        inputs: Some(vec![]),
288        outputs,
289        lock_time: None,
290        version: None,
291        labels: req.labels,
292        options: req.options.map(|o| CreateActionOptions {
293            accept_delayed_broadcast: o.accept_delayed_broadcast,
294            randomize_outputs: o.randomize_outputs,
295            sign_and_process: o.sign_and_process,
296            no_send: o.no_send,
297            ..Default::default()
298        }),
299    };
300
301    let output_count = args.outputs.as_ref().map(|o| o.len()).unwrap_or(0);
302    let total_sats: u64 = args
303        .outputs
304        .as_ref()
305        .map(|o| o.iter().map(|x| x.satoshis).sum())
306        .unwrap_or(0);
307
308    // Acquire spending lock — queues behind any in-flight createAction.
309    // Once the previous tx completes (and its change UTXO exists), we proceed.
310    let _guard = spending_lock.lock().await;
311    let result = wallet
312        .create_action(args, &originator)
313        .await
314        .map_err(AppError::from_wallet_error)?;
315    drop(_guard);
316
317    tracing::info!(
318        originator = %originator,
319        outputs = output_count,
320        total_satoshis = total_sats,
321        txid = ?result.txid.as_ref().map(|t| to_hex(t)),
322        "createAction"
323    );
324
325    // Build AtomicBEEF from result.beef (which has ancestors) + txid.
326    // result.tx is raw tx bytes; clients expect AtomicBEEF.
327    let atomic_beef = match (result.beef.as_ref(), result.txid.as_ref()) {
328        (Some(beef_bytes), Some(txid_bytes)) => {
329            let txid_hex = to_hex(txid_bytes);
330            match bsv_sdk::transaction::Beef::from_binary(beef_bytes) {
331                Ok(mut beef) => match beef.to_binary_atomic(&txid_hex) {
332                    Ok(ab) => Some(ab),
333                    Err(e) => {
334                        tracing::warn!(error = %e, "Failed to build AtomicBEEF, falling back to raw tx");
335                        result.tx.clone()
336                    }
337                },
338                Err(e) => {
339                    tracing::warn!(error = %e, "Failed to parse BEEF, falling back to raw tx");
340                    result.tx.clone()
341                }
342            }
343        }
344        _ => result.tx.clone(),
345    };
346
347    Ok(Json(McCreateActionRes {
348        txid: result.txid.map(|t| to_hex(&t)),
349        tx: atomic_beef,
350        send_with_results: result
351            .send_with_results
352            .and_then(|r| serde_json::to_value(r).ok()),
353        signable_transaction: result.signable_transaction.map(|st| {
354            // The SDK stores reference as Vec<u8> from String.into_bytes().
355            // Reverse that to get the original reference string that
356            // signAction/abortAction can look up in the pending tx cache.
357            let reference =
358                String::from_utf8(st.reference).unwrap_or_else(|e| hex::encode(e.into_bytes()));
359            McSignableTransaction {
360                tx: st.tx,
361                reference,
362            }
363        }),
364        no_send_change: result
365            .no_send_change
366            .and_then(|v| serde_json::to_value(v).ok()),
367    }))
368}
369
370/// POST /internalizeAction
371pub async fn internalize_action(
372    State(wallet): State<WalletState>,
373    headers: HeaderMap,
374    Json(req): Json<McInternalizeActionReq>,
375) -> Result<Json<McInternalizeActionRes>, AppError> {
376    let originator = extract_originator(&headers)?;
377
378    let args = InternalizeActionArgs {
379        tx: req.tx,
380        outputs: req
381            .outputs
382            .into_iter()
383            .map(|o| InternalizeOutput {
384                output_index: o.output_index,
385                protocol: o.protocol,
386                payment_remittance: o.payment_remittance.map(|p| WalletPayment {
387                    derivation_prefix: p.derivation_prefix,
388                    derivation_suffix: p.derivation_suffix,
389                    sender_identity_key: p.sender_identity_key,
390                }),
391                insertion_remittance: o.insertion_remittance.map(|i| BasketInsertion {
392                    basket: i.basket,
393                    custom_instructions: i.custom_instructions,
394                    tags: i.tags,
395                }),
396            })
397            .collect(),
398        description: req.description,
399        labels: None,
400        seek_permission: None,
401    };
402
403    let output_count = args.outputs.len();
404    let result = wallet
405        .internalize_action(args, &originator)
406        .await
407        .map_err(AppError::from_wallet_error)?;
408
409    tracing::info!(
410        originator = %originator,
411        outputs = output_count,
412        accepted = result.accepted,
413        "internalizeAction"
414    );
415
416    Ok(Json(McInternalizeActionRes {
417        accepted: result.accepted,
418    }))
419}
420
421// --- Helpers ---
422
423/// Parse [2, "protocol_name"] → Protocol
424fn parse_protocol_id(v: &serde_json::Value) -> Result<Protocol> {
425    let arr = v.as_array().context("protocolID must be an array")?;
426    let level = arr
427        .first()
428        .and_then(|v| v.as_u64())
429        .context("protocolID[0] must be an integer")?;
430    let name = arr
431        .get(1)
432        .and_then(|v| v.as_str())
433        .context("protocolID[1] must be a string")?;
434    Ok(Protocol::new(
435        match level {
436            0 => SecurityLevel::Silent,
437            1 => SecurityLevel::App,
438            2 => SecurityLevel::Counterparty,
439            _ => anyhow::bail!("invalid security level: {level}"),
440        },
441        name,
442    ))
443}
444
445/// Parse "03abc..." → Counterparty::Other(PublicKey)
446fn parse_counterparty(s: &str) -> Result<Counterparty> {
447    match s {
448        "self" => Ok(Counterparty::Self_),
449        "anyone" => Ok(Counterparty::Anyone),
450        hex => Ok(Counterparty::Other(PublicKey::from_hex(hex)?)),
451    }
452}
453
454// =============================================================================
455// Batch 1: Status (GET endpoints)
456// =============================================================================
457
458/// GET /getHeight
459pub async fn get_height(
460    State(wallet): State<WalletState>,
461) -> Result<Json<GetHeightResult>, AppError> {
462    let result = wallet
463        .get_height("localhost")
464        .await
465        .map_err(AppError::from_wallet_error)?;
466    Ok(Json(result))
467}
468
469/// GET /getNetwork
470pub async fn get_network(
471    State(wallet): State<WalletState>,
472) -> Result<Json<GetNetworkResult>, AppError> {
473    let result = wallet
474        .get_network("localhost")
475        .await
476        .map_err(AppError::from_wallet_error)?;
477    Ok(Json(result))
478}
479
480/// GET /getVersion
481pub async fn get_version(
482    State(wallet): State<WalletState>,
483) -> Result<Json<GetVersionResult>, AppError> {
484    let result = wallet
485        .get_version("localhost")
486        .await
487        .map_err(AppError::from_wallet_error)?;
488    Ok(Json(result))
489}
490
491/// GET /waitForAuthentication
492pub async fn wait_for_authentication(
493    State(wallet): State<WalletState>,
494) -> Result<Json<AuthenticatedResult>, AppError> {
495    let result = wallet
496        .wait_for_authentication("localhost")
497        .await
498        .map_err(AppError::from_wallet_error)?;
499    Ok(Json(result))
500}
501
502// =============================================================================
503// Batch 2: Header
504// =============================================================================
505
506/// POST /getHeaderForHeight
507pub async fn get_header_for_height(
508    State(wallet): State<WalletState>,
509    headers: HeaderMap,
510    Json(args): Json<GetHeaderArgs>,
511) -> Result<Json<GetHeaderResult>, AppError> {
512    let originator = extract_originator(&headers)?;
513    let result = wallet
514        .get_header_for_height(args, &originator)
515        .await
516        .map_err(AppError::from_wallet_error)?;
517    Ok(Json(result))
518}
519
520// =============================================================================
521// Batch 3: Crypto
522// =============================================================================
523
524/// POST /verifySignature
525pub async fn verify_signature(
526    State(wallet): State<WalletState>,
527    headers: HeaderMap,
528    Json(req): Json<McVerifySignatureReq>,
529) -> Result<Json<serde_json::Value>, AppError> {
530    let originator = extract_originator(&headers)?;
531    let args = VerifySignatureArgs {
532        data: req.data,
533        hash_to_directly_verify: None,
534        signature: req.signature,
535        protocol_id: parse_protocol_id(&req.protocol_id)?,
536        key_id: req.key_id,
537        counterparty: req
538            .counterparty
539            .map(|s| parse_counterparty(&s))
540            .transpose()?,
541        for_self: req.for_self,
542    };
543    let result = wallet
544        .verify_signature(args, &originator)
545        .await
546        .map_err(AppError::from_wallet_error)?;
547    Ok(Json(json!({ "valid": result.valid })))
548}
549
550/// POST /encrypt
551pub async fn encrypt(
552    State(wallet): State<WalletState>,
553    headers: HeaderMap,
554    Json(req): Json<McEncryptReq>,
555) -> Result<Json<serde_json::Value>, AppError> {
556    let originator = extract_originator(&headers)?;
557    let args = EncryptArgs {
558        plaintext: req.plaintext,
559        protocol_id: parse_protocol_id(&req.protocol_id)?,
560        key_id: req.key_id,
561        counterparty: req
562            .counterparty
563            .map(|s| parse_counterparty(&s))
564            .transpose()?,
565    };
566    let result = wallet
567        .encrypt(args, &originator)
568        .await
569        .map_err(AppError::from_wallet_error)?;
570    Ok(Json(json!({ "ciphertext": result.ciphertext })))
571}
572
573/// POST /decrypt
574pub async fn decrypt(
575    State(wallet): State<WalletState>,
576    headers: HeaderMap,
577    Json(req): Json<McDecryptReq>,
578) -> Result<Json<serde_json::Value>, AppError> {
579    let originator = extract_originator(&headers)?;
580    let args = DecryptArgs {
581        ciphertext: req.ciphertext,
582        protocol_id: parse_protocol_id(&req.protocol_id)?,
583        key_id: req.key_id,
584        counterparty: req
585            .counterparty
586            .map(|s| parse_counterparty(&s))
587            .transpose()?,
588    };
589    let result = wallet
590        .decrypt(args, &originator)
591        .await
592        .map_err(AppError::from_wallet_error)?;
593    Ok(Json(json!({ "plaintext": result.plaintext })))
594}
595
596/// POST /createHmac
597pub async fn create_hmac(
598    State(wallet): State<WalletState>,
599    headers: HeaderMap,
600    Json(req): Json<McCreateHmacReq>,
601) -> Result<Json<serde_json::Value>, AppError> {
602    let originator = extract_originator(&headers)?;
603    let args = CreateHmacArgs {
604        data: req.data,
605        protocol_id: parse_protocol_id(&req.protocol_id)?,
606        key_id: req.key_id,
607        counterparty: req
608            .counterparty
609            .map(|s| parse_counterparty(&s))
610            .transpose()?,
611    };
612    let result = wallet
613        .create_hmac(args, &originator)
614        .await
615        .map_err(AppError::from_wallet_error)?;
616    Ok(Json(json!({ "hmac": result.hmac.to_vec() })))
617}
618
619/// POST /verifyHmac
620pub async fn verify_hmac(
621    State(wallet): State<WalletState>,
622    headers: HeaderMap,
623    Json(req): Json<McVerifyHmacReq>,
624) -> Result<Json<serde_json::Value>, AppError> {
625    let originator = extract_originator(&headers)?;
626    let hmac: [u8; 32] = req
627        .hmac
628        .try_into()
629        .map_err(|_| anyhow::anyhow!("hmac must be exactly 32 bytes"))?;
630    let args = VerifyHmacArgs {
631        data: req.data,
632        hmac,
633        protocol_id: parse_protocol_id(&req.protocol_id)?,
634        key_id: req.key_id,
635        counterparty: req
636            .counterparty
637            .map(|s| parse_counterparty(&s))
638            .transpose()?,
639    };
640    let result = wallet
641        .verify_hmac(args, &originator)
642        .await
643        .map_err(AppError::from_wallet_error)?;
644    Ok(Json(json!({ "valid": result.valid })))
645}
646
647// =============================================================================
648// Batch 4: Transaction Workflow
649// =============================================================================
650
651/// POST /signAction
652pub async fn sign_action(
653    State(wallet): State<WalletState>,
654    headers: HeaderMap,
655    Json(args): Json<SignActionArgs>,
656) -> Result<Json<SignActionResult>, AppError> {
657    let originator = extract_originator(&headers)?;
658    let reference = args.reference.clone();
659    let result = wallet
660        .sign_action(args, &originator)
661        .await
662        .map_err(AppError::from_wallet_error)?;
663    tracing::info!(originator = %originator, reference = %reference, "signAction");
664    Ok(Json(result))
665}
666
667/// POST /abortAction
668pub async fn abort_action(
669    State(wallet): State<WalletState>,
670    headers: HeaderMap,
671    Json(args): Json<AbortActionArgs>,
672) -> Result<Json<AbortActionResult>, AppError> {
673    let originator = extract_originator(&headers)?;
674    let result = wallet
675        .abort_action(args, &originator)
676        .await
677        .map_err(AppError::from_wallet_error)?;
678    Ok(Json(result))
679}
680
681/// POST /listActions
682pub async fn list_actions(
683    State(wallet): State<WalletState>,
684    headers: HeaderMap,
685    Json(args): Json<ListActionsArgs>,
686) -> Result<Json<ListActionsResult>, AppError> {
687    let originator = extract_originator(&headers)?;
688    let result = wallet
689        .list_actions(args, &originator)
690        .await
691        .map_err(AppError::from_wallet_error)?;
692    Ok(Json(result))
693}
694
695/// POST /listOutputs
696pub async fn list_outputs(
697    State(wallet): State<WalletState>,
698    headers: HeaderMap,
699    Json(args): Json<ListOutputsArgs>,
700) -> Result<Json<ListOutputsResult>, AppError> {
701    let originator = extract_originator(&headers)?;
702    let result = wallet
703        .list_outputs(args, &originator)
704        .await
705        .map_err(AppError::from_wallet_error)?;
706    Ok(Json(result))
707}
708
709/// POST /relinquishOutput
710///
711/// Acquires SpendingLock because relinquish modifies UTXO state and can race
712/// with createAction on SQLite writes (previously caused SQLITE_BUSY errors).
713pub async fn relinquish_output(
714    State(wallet): State<WalletState>,
715    axum::Extension(spending_lock): axum::Extension<super::SpendingLock>,
716    headers: HeaderMap,
717    Json(args): Json<RelinquishOutputArgs>,
718) -> Result<Json<RelinquishOutputResult>, AppError> {
719    let originator = extract_originator(&headers)?;
720    let _guard = spending_lock.lock().await;
721    let result = wallet
722        .relinquish_output(args, &originator)
723        .await
724        .map_err(AppError::from_wallet_error)?;
725    Ok(Json(result))
726}
727
728// =============================================================================
729// Batch 5: Certificates
730// =============================================================================
731
732/// POST /acquireCertificate
733pub async fn acquire_certificate(
734    State(wallet): State<WalletState>,
735    headers: HeaderMap,
736    Json(args): Json<AcquireCertificateArgs>,
737) -> Result<Json<WalletCertificate>, AppError> {
738    let originator = extract_originator(&headers)?;
739    let result = wallet
740        .acquire_certificate(args, &originator)
741        .await
742        .map_err(AppError::from_wallet_error)?;
743    Ok(Json(result))
744}
745
746/// POST /listCertificates
747pub async fn list_certificates(
748    State(wallet): State<WalletState>,
749    headers: HeaderMap,
750    Json(args): Json<ListCertificatesArgs>,
751) -> Result<Json<ListCertificatesResult>, AppError> {
752    let originator = extract_originator(&headers)?;
753    let result = wallet
754        .list_certificates(args, &originator)
755        .await
756        .map_err(AppError::from_wallet_error)?;
757    Ok(Json(result))
758}
759
760/// POST /proveCertificate
761pub async fn prove_certificate(
762    State(wallet): State<WalletState>,
763    headers: HeaderMap,
764    Json(args): Json<ProveCertificateArgs>,
765) -> Result<Json<ProveCertificateResult>, AppError> {
766    let originator = extract_originator(&headers)?;
767    let result = wallet
768        .prove_certificate(args, &originator)
769        .await
770        .map_err(AppError::from_wallet_error)?;
771    Ok(Json(result))
772}
773
774/// POST /relinquishCertificate
775pub async fn relinquish_certificate(
776    State(wallet): State<WalletState>,
777    headers: HeaderMap,
778    Json(args): Json<RelinquishCertificateArgs>,
779) -> Result<Json<RelinquishCertificateResult>, AppError> {
780    let originator = extract_originator(&headers)?;
781    let result = wallet
782        .relinquish_certificate(args, &originator)
783        .await
784        .map_err(AppError::from_wallet_error)?;
785    Ok(Json(result))
786}
787
788// =============================================================================
789// Batch 6: Discovery + Key Linkage
790// =============================================================================
791
792/// POST /discoverByIdentityKey
793pub async fn discover_by_identity_key(
794    State(wallet): State<WalletState>,
795    headers: HeaderMap,
796    Json(args): Json<DiscoverByIdentityKeyArgs>,
797) -> Result<Json<DiscoverCertificatesResult>, AppError> {
798    let originator = extract_originator(&headers)?;
799    let result = wallet
800        .discover_by_identity_key(args, &originator)
801        .await
802        .map_err(AppError::from_wallet_error)?;
803    Ok(Json(result))
804}
805
806/// POST /discoverByAttributes
807pub async fn discover_by_attributes(
808    State(wallet): State<WalletState>,
809    headers: HeaderMap,
810    Json(args): Json<DiscoverByAttributesArgs>,
811) -> Result<Json<DiscoverCertificatesResult>, AppError> {
812    let originator = extract_originator(&headers)?;
813    let result = wallet
814        .discover_by_attributes(args, &originator)
815        .await
816        .map_err(AppError::from_wallet_error)?;
817    Ok(Json(result))
818}
819
820/// POST /revealCounterpartyKeyLinkage
821pub async fn reveal_counterparty_key_linkage(
822    State(wallet): State<WalletState>,
823    headers: HeaderMap,
824    Json(req): Json<McRevealCounterpartyKeyLinkageReq>,
825) -> Result<Json<RevealCounterpartyKeyLinkageResult>, AppError> {
826    let originator = extract_originator(&headers)?;
827    let args = WalletRevealCounterpartyArgs {
828        counterparty: PublicKey::from_hex(&req.counterparty)
829            .map_err(|e| anyhow::anyhow!("{}", e))?,
830        verifier: PublicKey::from_hex(&req.verifier).map_err(|e| anyhow::anyhow!("{}", e))?,
831        privileged: req.privileged,
832        privileged_reason: req.privileged_reason,
833    };
834    let result = wallet
835        .reveal_counterparty_key_linkage(args, &originator)
836        .await
837        .map_err(AppError::from_wallet_error)?;
838    Ok(Json(result))
839}
840
841/// POST /revealSpecificKeyLinkage
842pub async fn reveal_specific_key_linkage(
843    State(wallet): State<WalletState>,
844    headers: HeaderMap,
845    Json(req): Json<McRevealSpecificKeyLinkageReq>,
846) -> Result<Json<RevealSpecificKeyLinkageResult>, AppError> {
847    let originator = extract_originator(&headers)?;
848    let args = WalletRevealSpecificArgs {
849        counterparty: parse_counterparty(&req.counterparty)?,
850        verifier: PublicKey::from_hex(&req.verifier).map_err(|e| anyhow::anyhow!("{}", e))?,
851        protocol_id: parse_protocol_id(&req.protocol_id)?,
852        key_id: req.key_id,
853        privileged: req.privileged,
854        privileged_reason: req.privileged_reason,
855    };
856    let result = wallet
857        .reveal_specific_key_linkage(args, &originator)
858        .await
859        .map_err(AppError::from_wallet_error)?;
860    Ok(Json(result))
861}