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 AbortActionArgs,
10 AbortActionResult,
11 AcquireCertificateArgs,
13 AuthenticatedResult,
15 BasketInsertion,
17 Counterparty,
19 CreateActionArgs,
20 CreateActionInput,
21 CreateActionOptions,
22 CreateActionOutput,
23 CreateHmacArgs,
24 CreateSignatureArgs,
25 DecryptArgs,
27 DiscoverByAttributesArgs,
29 DiscoverByIdentityKeyArgs,
30 DiscoverCertificatesResult,
31 EncryptArgs,
32 GetHeaderArgs,
34 GetHeaderResult,
35 GetHeightResult,
36 GetNetworkResult,
37 GetPublicKeyArgs,
38 GetVersionResult,
39 InternalizeActionArgs,
40 InternalizeOutput,
41 ListActionsArgs,
42 ListActionsResult,
43 ListCertificatesArgs,
44 ListCertificatesResult,
45 ListOutputsArgs,
47 ListOutputsResult,
48 Outpoint,
49 Protocol,
50 ProveCertificateArgs,
51 ProveCertificateResult,
52 RelinquishCertificateArgs,
53 RelinquishCertificateResult,
54 RelinquishOutputArgs,
55 RelinquishOutputResult,
56 RevealCounterpartyKeyLinkageResult,
58 RevealSpecificKeyLinkageResult,
59 SecurityLevel,
60 SignActionArgs,
61 TrustSelf,
62 VerifyHmacArgs,
63 VerifySignatureArgs,
64 WalletCertificate,
65 WalletInterface,
66 WalletPayment,
67 WalletRevealCounterpartyArgs,
68 WalletRevealSpecificArgs,
69};
70use bsv_wallet_toolbox::{Services, StorageSqlx, Wallet};
71use serde_json::json;
72use std::sync::Arc;
73
74use super::types::*;
75
76pub type WalletState = Arc<Wallet<StorageSqlx, Services>>;
77
78use super::audit;
79
80fn cp_str(c: &Counterparty) -> String {
84 match c {
88 Counterparty::Self_ => "self".to_string(),
89 Counterparty::Anyone => "anyone".to_string(),
90 Counterparty::Other(pk) => pk.to_string().to_lowercase(),
91 }
92}
93
94fn level_num(l: SecurityLevel) -> u8 {
95 match l {
96 SecurityLevel::Silent => 0,
97 SecurityLevel::App => 1,
98 SecurityLevel::Counterparty => 2,
99 }
100}
101
102fn extract_originator(
104 headers: &HeaderMap,
105) -> Result<String, (StatusCode, Json<serde_json::Value>)> {
106 if let Some(origin) = headers.get("origin").and_then(|v| v.to_str().ok()) {
107 if let Ok(url) = url::Url::parse(origin) {
108 if let Some(host) = url.host_str() {
109 return Ok(host.to_string());
110 }
111 }
112 }
113 if let Some(originator) = headers.get("originator").and_then(|v| v.to_str().ok()) {
114 return Ok(originator.to_string());
115 }
116 Err((
117 StatusCode::BAD_REQUEST,
118 Json(json!({"message": "Origin header required"})),
119 ))
120}
121
122pub struct AppError {
124 status: StatusCode,
125 code: &'static str,
126 message: String,
127}
128
129impl AppError {
130 fn classify(msg: &str) -> (StatusCode, &'static str) {
132 let clean = msg.strip_prefix("wallet error: ").unwrap_or(msg);
134
135 if clean.starts_with("Insufficient funds:") || clean.starts_with("insufficient funds:") {
136 (StatusCode::PAYMENT_REQUIRED, "INSUFFICIENT_FUNDS")
137 } else if clean.starts_with("Authentication required")
138 || clean.starts_with("Invalid identity key:")
139 {
140 (StatusCode::UNAUTHORIZED, "AUTHENTICATION_REQUIRED")
141 } else if clean.starts_with("Access denied:") {
142 (StatusCode::FORBIDDEN, "ACCESS_DENIED")
143 } else if clean.starts_with("Entity not found:") || clean.starts_with("not found:") {
144 (StatusCode::NOT_FOUND, "NOT_FOUND")
145 } else if clean.starts_with("Duplicate entity:") || clean.starts_with("Invalid operation:")
146 {
147 (StatusCode::CONFLICT, "CONFLICT")
148 } else if clean.starts_with("Validation error:")
149 || clean.starts_with("Invalid argument:")
150 || clean.starts_with("Origin header required")
151 {
152 (StatusCode::BAD_REQUEST, "VALIDATION_ERROR")
153 } else if clean.starts_with("Storage error:")
154 || clean.starts_with("Database error:")
155 || clean.starts_with("Internal error:")
156 {
157 (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR")
158 } else if clean.starts_with("Transaction broadcast failed") {
159 (StatusCode::BAD_GATEWAY, "BROADCAST_REJECTED")
164 } else if clean.starts_with("Service error:")
165 || clean.starts_with("Network error:")
166 || clean.starts_with("Broadcast failed:")
167 {
168 (StatusCode::BAD_GATEWAY, "SERVICE_ERROR")
169 } else {
170 (StatusCode::BAD_REQUEST, "ERROR")
171 }
172 }
173
174 fn from_wallet_error(e: impl std::fmt::Display) -> Self {
176 let message = e.to_string();
177 let (status, code) = Self::classify(&message);
178 Self {
179 status,
180 code,
181 message,
182 }
183 }
184
185 fn message(&self) -> &str {
186 &self.message
187 }
188
189 fn with_hint(mut self, hint: &str) -> Self {
191 self.message = format!("{} (hint: {})", self.message, hint);
192 self
193 }
194
195 fn broadcast_rejected(txid: &str) -> Self {
200 Self {
201 status: StatusCode::BAD_GATEWAY,
202 code: "BROADCAST_REJECTED",
203 message: format!(
204 "broadcast rejected: transaction {txid} is absent from BOTH the broadcaster \
205 it was submitted to AND an independent chain index, after the full probe \
206 window. The broadcaster dropped it — most likely error 465 \"fee too low\" \
207 on a deep unconfirmed BEEF. The funds were NOT sent."
208 ),
209 }
210 }
211}
212
213impl From<anyhow::Error> for AppError {
214 fn from(err: anyhow::Error) -> Self {
215 Self::from_wallet_error(err)
216 }
217}
218
219impl From<(StatusCode, Json<serde_json::Value>)> for AppError {
220 fn from((status, json): (StatusCode, Json<serde_json::Value>)) -> Self {
221 let message = json
222 .0
223 .get("message")
224 .and_then(|m| m.as_str())
225 .unwrap_or_default()
226 .to_string();
227 let (_, code) = Self::classify(&message);
228 Self {
229 status,
230 code,
231 message,
232 }
233 }
234}
235
236impl IntoResponse for AppError {
237 fn into_response(self) -> Response {
238 tracing::warn!(
239 status = self.status.as_u16(),
240 code = self.code,
241 message = %self.message,
242 "request error"
243 );
244 (
245 self.status,
246 Json(json!({"code": self.code, "message": self.message})),
247 )
248 .into_response()
249 }
250}
251
252pub async fn is_authenticated() -> Json<serde_json::Value> {
254 Json(json!({"authenticated": true}))
255}
256
257pub async fn get_public_key(
259 State(wallet): State<WalletState>,
260 headers: HeaderMap,
261 Json(req): Json<McGetPublicKeyReq>,
262) -> Result<Json<McGetPublicKeyRes>, AppError> {
263 let originator = extract_originator(&headers)?;
264
265 let args = GetPublicKeyArgs {
266 identity_key: req.identity_key,
267 protocol_id: req.protocol_id.map(|v| parse_protocol_id(&v)).transpose()?,
268 key_id: req.key_id,
269 counterparty: req
270 .counterparty
271 .map(|s| parse_counterparty(&s))
272 .transpose()?,
273 for_self: req.for_self,
274 };
275
276 if let Some(p) = args.protocol_id.as_ref() {
277 audit::record(audit::protocol_entry(
278 "getPublicKey",
279 level_num(p.security_level),
280 &p.protocol_name,
281 args.counterparty.as_ref().map(cp_str),
282 args.key_id.clone(),
283 &originator,
284 ));
285 }
286 let result = wallet
287 .get_public_key(args, &originator)
288 .await
289 .map_err(AppError::from_wallet_error)?;
290
291 Ok(Json(McGetPublicKeyRes {
292 public_key: result.public_key,
293 }))
294}
295
296pub async fn create_signature(
298 State(wallet): State<WalletState>,
299 headers: HeaderMap,
300 Json(req): Json<McCreateSignatureReq>,
301) -> Result<Json<McCreateSignatureRes>, AppError> {
302 let originator = extract_originator(&headers)?;
303
304 let hash_to_directly_sign = req
307 .hash_to_directly_sign
308 .map(|h| {
309 <[u8; 32]>::try_from(h.as_slice()).map_err(|_| {
310 AppError::from_wallet_error("Validation error: hashToDirectlySign must be 32 bytes")
311 })
312 })
313 .transpose()?;
314 if req.data.is_none() && hash_to_directly_sign.is_none() {
315 return Err(AppError::from_wallet_error(
316 "Validation error: one of data or hashToDirectlySign is required",
317 ));
318 }
319
320 let args = CreateSignatureArgs {
321 data: req.data,
322 hash_to_directly_sign,
323 protocol_id: parse_protocol_id(&req.protocol_id)?,
324 key_id: req.key_id,
325 counterparty: Some(parse_counterparty(&req.counterparty)?),
326 };
327
328 audit::record(audit::protocol_entry(
329 "createSignature",
330 level_num(args.protocol_id.security_level),
331 &args.protocol_id.protocol_name,
332 args.counterparty.as_ref().map(cp_str),
333 Some(args.key_id.clone()),
334 &originator,
335 ));
336 let result = wallet
337 .create_signature(args, &originator)
338 .await
339 .map_err(AppError::from_wallet_error)?;
340
341 Ok(Json(McCreateSignatureRes {
342 signature: result.signature,
343 }))
344}
345
346pub async fn create_action(
352 State(wallet): State<WalletState>,
353 axum::Extension(spending_lock): axum::Extension<super::SpendingLock>,
354 axum::Extension(verifier): axum::Extension<crate::broadcast_verify::BroadcastVerifier>,
355 headers: HeaderMap,
356 Json(req): Json<McCreateActionReq>,
357) -> Result<Json<McCreateActionRes>, AppError> {
358 let originator = extract_originator(&headers)?;
359
360 let (opt_no_send, opt_accept_delayed) = req
363 .options
364 .as_ref()
365 .map(|o| {
366 (
367 o.no_send.unwrap_or(false),
368 o.accept_delayed_broadcast.unwrap_or(false),
369 )
370 })
371 .unwrap_or((false, false));
372
373 let outputs = req
374 .outputs
375 .map(|outs| {
376 outs.into_iter()
377 .map(|o| {
378 Ok(CreateActionOutput {
379 locking_script: hex::decode(&o.locking_script)?,
380 satoshis: o.satoshis,
381 output_description: o.output_description,
382 basket: o.basket,
383 custom_instructions: o.custom_instructions,
384 tags: o.tags,
385 })
386 })
387 .collect::<Result<Vec<_>>>()
388 })
389 .transpose()?;
390
391 let inputs = match req.inputs {
397 Some(reqs) if !reqs.is_empty() => {
398 let mut mapped = Vec::with_capacity(reqs.len());
399 for i in reqs {
400 let outpoint = Outpoint::from_string(&i.outpoint)
401 .map_err(|e| anyhow::anyhow!("invalid input outpoint '{}': {e}", i.outpoint))?;
402 let unlocking_script = match i.unlocking_script {
403 Some(h) => Some(hex::decode(&h).context("invalid unlockingScript hex")?),
404 None => None,
405 };
406 mapped.push(CreateActionInput {
407 outpoint,
408 input_description: i.input_description.unwrap_or_else(|| "input".to_string()),
409 unlocking_script,
410 unlocking_script_length: i.unlocking_script_length,
411 sequence_number: i.sequence_number,
412 });
413 }
414 Some(mapped)
415 }
416 _ => Some(vec![]),
417 };
418
419 let args = CreateActionArgs {
420 description: req.description,
421 input_beef: req.input_beef,
422 inputs,
423 outputs,
424 lock_time: req.lock_time,
425 version: None,
426 labels: req.labels,
427 options: req.options.map(|o| CreateActionOptions {
428 accept_delayed_broadcast: o.accept_delayed_broadcast,
429 randomize_outputs: o.randomize_outputs,
430 sign_and_process: o.sign_and_process,
431 no_send: o.no_send,
432 trust_self: match o.trust_self.as_deref() {
433 Some("known") => Some(TrustSelf::Known),
434 _ => None,
435 },
436 ..Default::default()
437 }),
438 };
439
440 let output_count = args.outputs.as_ref().map(|o| o.len()).unwrap_or(0);
441 let total_sats: u64 = args
442 .outputs
443 .as_ref()
444 .map(|o| o.iter().map(|x| x.satoshis).sum())
445 .unwrap_or(0);
446
447 audit::record(audit::spend_entry(
451 "createAction",
452 total_sats,
453 Some(args.description.clone()),
454 &originator,
455 ));
456
457 let _guard = spending_lock.lock().await;
460 let result = wallet
461 .create_action(args, &originator)
462 .await
463 .map_err(AppError::from_wallet_error)?;
464 drop(_guard);
465
466 tracing::info!(
467 originator = %originator,
468 outputs = output_count,
469 total_satoshis = total_sats,
470 txid = ?result.txid.as_ref().map(|t| to_hex(t)),
471 "createAction"
472 );
473
474 let atomic_beef = match (result.beef.as_ref(), result.txid.as_ref()) {
477 (Some(beef_bytes), Some(txid_bytes)) => {
478 let txid_hex = to_hex(txid_bytes);
479 match bsv_sdk::transaction::Beef::from_binary(beef_bytes) {
480 Ok(mut beef) => match beef.to_binary_atomic(&txid_hex) {
481 Ok(ab) => Some(ab),
482 Err(e) => {
483 tracing::warn!(error = %e, "Failed to build AtomicBEEF, falling back to raw tx");
484 result.tx.clone()
485 }
486 },
487 Err(e) => {
488 tracing::warn!(error = %e, "Failed to parse BEEF, falling back to raw tx");
489 result.tx.clone()
490 }
491 }
492 }
493 _ => result.tx.clone(),
494 };
495
496 let disposition =
508 super::broadcast_follow_up::disposition(&result, opt_no_send, opt_accept_delayed);
509 if let Some(txid_bytes) = result.txid.as_ref() {
510 let txid_hex = to_hex(txid_bytes);
511 let probe = verifier.clone();
512 let report_wallet = wallet.clone();
513 let ancestors: Vec<String> = result
516 .beef
517 .as_deref()
518 .map(|beef| bsv_wallet_toolbox::unproven_ancestors_in_beef(beef, &txid_hex))
519 .unwrap_or_default();
520 let outcome = super::broadcast_follow_up::follow_up(
521 disposition,
522 txid_hex.clone(),
523 move |txid| async move { probe.verify_report(&txid).await },
524 move |txid, report| async move {
525 super::broadcast_follow_up::apply_presence_report(
526 report_wallet.storage(),
527 report_wallet.services(),
528 &txid,
529 &ancestors,
530 &report,
531 )
532 .await;
533 },
534 )
535 .await;
536 if outcome == super::broadcast_follow_up::FollowUp::Rejected {
537 return Err(AppError::broadcast_rejected(&txid_hex));
538 }
539 }
540
541 Ok(Json(McCreateActionRes {
542 txid: result.txid.map(|t| to_hex(&t)),
543 tx: atomic_beef,
544 send_with_results: result
545 .send_with_results
546 .and_then(|r| serde_json::to_value(r).ok()),
547 signable_transaction: result.signable_transaction.map(|st| {
548 let reference =
552 String::from_utf8(st.reference).unwrap_or_else(|e| hex::encode(e.into_bytes()));
553 McSignableTransaction {
554 tx: st.tx,
555 reference,
556 }
557 }),
558 no_send_change: result
559 .no_send_change
560 .and_then(|v| serde_json::to_value(v).ok()),
561 }))
562}
563
564pub async fn internalize_action(
566 State(wallet): State<WalletState>,
567 headers: HeaderMap,
568 Json(req): Json<McInternalizeActionReq>,
569) -> Result<Json<McInternalizeActionRes>, AppError> {
570 let originator = extract_originator(&headers)?;
571
572 let args = InternalizeActionArgs {
573 tx: req.tx,
574 outputs: req
575 .outputs
576 .into_iter()
577 .map(|o| InternalizeOutput {
578 output_index: o.output_index,
579 protocol: o.protocol,
580 payment_remittance: o.payment_remittance.map(|p| WalletPayment {
581 derivation_prefix: p.derivation_prefix,
582 derivation_suffix: p.derivation_suffix,
583 sender_identity_key: p.sender_identity_key,
584 }),
585 insertion_remittance: o.insertion_remittance.map(|i| BasketInsertion {
586 basket: i.basket,
587 custom_instructions: i.custom_instructions,
588 tags: i.tags,
589 }),
590 })
591 .collect(),
592 description: req.description,
593 labels: req.labels,
594 seek_permission: None,
595 };
596
597 let output_count = args.outputs.len();
598 let result = wallet
599 .internalize_action(args, &originator)
600 .await
601 .map_err(AppError::from_wallet_error)?;
602
603 tracing::info!(
604 originator = %originator,
605 outputs = output_count,
606 accepted = result.accepted,
607 "internalizeAction"
608 );
609
610 Ok(Json(McInternalizeActionRes {
611 accepted: result.accepted,
612 }))
613}
614
615fn parse_protocol_id(v: &serde_json::Value) -> Result<Protocol> {
619 let arr = v.as_array().context("protocolID must be an array")?;
620 let level = arr
621 .first()
622 .and_then(|v| v.as_u64())
623 .context("protocolID[0] must be an integer")?;
624 let name = arr
625 .get(1)
626 .and_then(|v| v.as_str())
627 .context("protocolID[1] must be a string")?;
628 Ok(Protocol::new(
629 match level {
630 0 => SecurityLevel::Silent,
631 1 => SecurityLevel::App,
632 2 => SecurityLevel::Counterparty,
633 _ => anyhow::bail!("invalid security level: {level}"),
634 },
635 name,
636 ))
637}
638
639fn parse_counterparty(s: &str) -> Result<Counterparty> {
641 match s {
642 "self" => Ok(Counterparty::Self_),
643 "anyone" => Ok(Counterparty::Anyone),
644 hex => Ok(Counterparty::Other(PublicKey::from_hex(hex)?)),
645 }
646}
647
648pub async fn get_height(
654 State(wallet): State<WalletState>,
655) -> Result<Json<GetHeightResult>, AppError> {
656 let result = wallet
657 .get_height("localhost")
658 .await
659 .map_err(AppError::from_wallet_error)?;
660 Ok(Json(result))
661}
662
663pub async fn get_network(
665 State(wallet): State<WalletState>,
666) -> Result<Json<GetNetworkResult>, AppError> {
667 let result = wallet
668 .get_network("localhost")
669 .await
670 .map_err(AppError::from_wallet_error)?;
671 Ok(Json(result))
672}
673
674pub async fn get_version(
676 State(wallet): State<WalletState>,
677) -> Result<Json<GetVersionResult>, AppError> {
678 let result = wallet
679 .get_version("localhost")
680 .await
681 .map_err(AppError::from_wallet_error)?;
682 Ok(Json(result))
683}
684
685pub async fn wait_for_authentication(
687 State(wallet): State<WalletState>,
688) -> Result<Json<AuthenticatedResult>, AppError> {
689 let result = wallet
690 .wait_for_authentication("localhost")
691 .await
692 .map_err(AppError::from_wallet_error)?;
693 Ok(Json(result))
694}
695
696pub async fn get_header_for_height(
702 State(wallet): State<WalletState>,
703 headers: HeaderMap,
704 Json(args): Json<GetHeaderArgs>,
705) -> Result<Json<GetHeaderResult>, AppError> {
706 let originator = extract_originator(&headers)?;
707 let result = wallet
708 .get_header_for_height(args, &originator)
709 .await
710 .map_err(AppError::from_wallet_error)?;
711 Ok(Json(result))
712}
713
714pub async fn verify_signature(
720 State(wallet): State<WalletState>,
721 headers: HeaderMap,
722 Json(req): Json<McVerifySignatureReq>,
723) -> Result<Json<serde_json::Value>, AppError> {
724 let originator = extract_originator(&headers)?;
725 let args = VerifySignatureArgs {
726 data: req.data,
727 hash_to_directly_verify: None,
728 signature: req.signature,
729 protocol_id: parse_protocol_id(&req.protocol_id)?,
730 key_id: req.key_id,
731 counterparty: req
732 .counterparty
733 .map(|s| parse_counterparty(&s))
734 .transpose()?,
735 for_self: req.for_self,
736 };
737 let result = wallet
738 .verify_signature(args, &originator)
739 .await
740 .map_err(AppError::from_wallet_error)?;
741 Ok(Json(json!({ "valid": result.valid })))
742}
743
744pub async fn encrypt(
746 State(wallet): State<WalletState>,
747 headers: HeaderMap,
748 Json(req): Json<McEncryptReq>,
749) -> Result<Json<serde_json::Value>, AppError> {
750 let originator = extract_originator(&headers)?;
751 let args = EncryptArgs {
752 plaintext: req.plaintext,
753 protocol_id: parse_protocol_id(&req.protocol_id)?,
754 key_id: req.key_id,
755 counterparty: req
756 .counterparty
757 .map(|s| parse_counterparty(&s))
758 .transpose()?,
759 };
760 audit::record(audit::protocol_entry(
761 "encrypt",
762 level_num(args.protocol_id.security_level),
763 &args.protocol_id.protocol_name,
764 args.counterparty.as_ref().map(cp_str),
765 Some(args.key_id.clone()),
766 &originator,
767 ));
768 let result = wallet
769 .encrypt(args, &originator)
770 .await
771 .map_err(AppError::from_wallet_error)?;
772 Ok(Json(json!({ "ciphertext": result.ciphertext })))
773}
774
775pub async fn decrypt(
777 State(wallet): State<WalletState>,
778 headers: HeaderMap,
779 Json(req): Json<McDecryptReq>,
780) -> Result<Json<serde_json::Value>, AppError> {
781 let originator = extract_originator(&headers)?;
782 let args = DecryptArgs {
783 ciphertext: req.ciphertext,
784 protocol_id: parse_protocol_id(&req.protocol_id)?,
785 key_id: req.key_id,
786 counterparty: req
787 .counterparty
788 .map(|s| parse_counterparty(&s))
789 .transpose()?,
790 };
791 audit::record(audit::protocol_entry(
792 "decrypt",
793 level_num(args.protocol_id.security_level),
794 &args.protocol_id.protocol_name,
795 args.counterparty.as_ref().map(cp_str),
796 Some(args.key_id.clone()),
797 &originator,
798 ));
799 let result = wallet
800 .decrypt(args, &originator)
801 .await
802 .map_err(AppError::from_wallet_error)?;
803 Ok(Json(json!({ "plaintext": result.plaintext })))
804}
805
806pub async fn create_hmac(
808 State(wallet): State<WalletState>,
809 headers: HeaderMap,
810 Json(req): Json<McCreateHmacReq>,
811) -> Result<Json<serde_json::Value>, AppError> {
812 let originator = extract_originator(&headers)?;
813 let args = CreateHmacArgs {
814 data: req.data,
815 protocol_id: parse_protocol_id(&req.protocol_id)?,
816 key_id: req.key_id,
817 counterparty: req
818 .counterparty
819 .map(|s| parse_counterparty(&s))
820 .transpose()?,
821 };
822 audit::record(audit::protocol_entry(
823 "createHmac",
824 level_num(args.protocol_id.security_level),
825 &args.protocol_id.protocol_name,
826 args.counterparty.as_ref().map(cp_str),
827 Some(args.key_id.clone()),
828 &originator,
829 ));
830 let result = wallet
831 .create_hmac(args, &originator)
832 .await
833 .map_err(AppError::from_wallet_error)?;
834 Ok(Json(json!({ "hmac": result.hmac.to_vec() })))
835}
836
837pub async fn verify_hmac(
839 State(wallet): State<WalletState>,
840 headers: HeaderMap,
841 Json(req): Json<McVerifyHmacReq>,
842) -> Result<Json<serde_json::Value>, AppError> {
843 let originator = extract_originator(&headers)?;
844 let hmac: [u8; 32] = req
845 .hmac
846 .try_into()
847 .map_err(|_| anyhow::anyhow!("hmac must be exactly 32 bytes"))?;
848 let args = VerifyHmacArgs {
849 data: req.data,
850 hmac,
851 protocol_id: parse_protocol_id(&req.protocol_id)?,
852 key_id: req.key_id,
853 counterparty: req
854 .counterparty
855 .map(|s| parse_counterparty(&s))
856 .transpose()?,
857 };
858 let result = wallet
859 .verify_hmac(args, &originator)
860 .await
861 .map_err(AppError::from_wallet_error)?;
862 Ok(Json(json!({ "valid": result.valid })))
863}
864
865pub async fn sign_action(
871 State(wallet): State<WalletState>,
872 headers: HeaderMap,
873 Json(args): Json<SignActionArgs>,
874) -> Result<Json<McSignActionRes>, AppError> {
875 let originator = extract_originator(&headers)?;
876 let reference = args.reference.clone();
877 let result = wallet
878 .sign_action(args, &originator)
879 .await
880 .map_err(AppError::from_wallet_error)?;
881 tracing::info!(originator = %originator, reference = %reference, "signAction");
882 Ok(Json(McSignActionRes {
886 txid: result.txid.map(|t| to_hex(&t)),
887 tx: result.tx,
888 send_with_results: result
889 .send_with_results
890 .and_then(|r| serde_json::to_value(r).ok()),
891 }))
892}
893
894pub async fn abort_action(
896 State(wallet): State<WalletState>,
897 headers: HeaderMap,
898 Json(args): Json<AbortActionArgs>,
899) -> Result<Json<AbortActionResult>, AppError> {
900 let originator = extract_originator(&headers)?;
901 let result = wallet.abort_action(args, &originator).await.map_err(|e| {
902 let mut err = AppError::from_wallet_error(e);
909 if err.message().contains("the network has transaction") {
910 err = err.with_hint(
911 "the wallet holds chain evidence for this transaction, so it \
912 cannot be aborted; if it is a phantom after all, \
913 `bsv-wallet reconcile-broadcasts --execute` re-checks it \
914 against the chain index",
915 );
916 }
917 err
918 })?;
919 Ok(Json(result))
920}
921
922pub async fn list_actions(
924 State(wallet): State<WalletState>,
925 headers: HeaderMap,
926 Json(args): Json<ListActionsArgs>,
927) -> Result<Json<ListActionsResult>, AppError> {
928 let originator = extract_originator(&headers)?;
929 let result = wallet
930 .list_actions(args, &originator)
931 .await
932 .map_err(AppError::from_wallet_error)?;
933 Ok(Json(result))
934}
935
936pub async fn list_outputs(
938 State(wallet): State<WalletState>,
939 headers: HeaderMap,
940 Json(args): Json<ListOutputsArgs>,
941) -> Result<Json<ListOutputsResult>, AppError> {
942 let originator = extract_originator(&headers)?;
943 let result = wallet
944 .list_outputs(args, &originator)
945 .await
946 .map_err(AppError::from_wallet_error)?;
947 Ok(Json(result))
948}
949
950pub async fn relinquish_output(
955 State(wallet): State<WalletState>,
956 axum::Extension(spending_lock): axum::Extension<super::SpendingLock>,
957 headers: HeaderMap,
958 Json(args): Json<RelinquishOutputArgs>,
959) -> Result<Json<RelinquishOutputResult>, AppError> {
960 let originator = extract_originator(&headers)?;
961 let _guard = spending_lock.lock().await;
962 let result = wallet
963 .relinquish_output(args, &originator)
964 .await
965 .map_err(AppError::from_wallet_error)?;
966 Ok(Json(result))
967}
968
969pub async fn acquire_certificate(
975 State(wallet): State<WalletState>,
976 headers: HeaderMap,
977 Json(args): Json<AcquireCertificateArgs>,
978) -> Result<Json<WalletCertificate>, AppError> {
979 let originator = extract_originator(&headers)?;
980 let result = wallet
981 .acquire_certificate(args, &originator)
982 .await
983 .map_err(AppError::from_wallet_error)?;
984 Ok(Json(result))
985}
986
987pub async fn list_certificates(
989 State(wallet): State<WalletState>,
990 headers: HeaderMap,
991 Json(args): Json<ListCertificatesArgs>,
992) -> Result<Json<ListCertificatesResult>, AppError> {
993 let originator = extract_originator(&headers)?;
994 let result = wallet
995 .list_certificates(args, &originator)
996 .await
997 .map_err(AppError::from_wallet_error)?;
998 Ok(Json(result))
999}
1000
1001pub async fn prove_certificate(
1003 State(wallet): State<WalletState>,
1004 headers: HeaderMap,
1005 Json(args): Json<ProveCertificateArgs>,
1006) -> Result<Json<ProveCertificateResult>, AppError> {
1007 let originator = extract_originator(&headers)?;
1008 let result = wallet
1009 .prove_certificate(args, &originator)
1010 .await
1011 .map_err(AppError::from_wallet_error)?;
1012 Ok(Json(result))
1013}
1014
1015pub async fn relinquish_certificate(
1017 State(wallet): State<WalletState>,
1018 headers: HeaderMap,
1019 Json(args): Json<RelinquishCertificateArgs>,
1020) -> Result<Json<RelinquishCertificateResult>, AppError> {
1021 let originator = extract_originator(&headers)?;
1022 let result = wallet
1023 .relinquish_certificate(args, &originator)
1024 .await
1025 .map_err(AppError::from_wallet_error)?;
1026 Ok(Json(result))
1027}
1028
1029pub async fn discover_by_identity_key(
1035 State(wallet): State<WalletState>,
1036 headers: HeaderMap,
1037 Json(args): Json<DiscoverByIdentityKeyArgs>,
1038) -> Result<Json<DiscoverCertificatesResult>, AppError> {
1039 let originator = extract_originator(&headers)?;
1040 let result = wallet
1041 .discover_by_identity_key(args, &originator)
1042 .await
1043 .map_err(AppError::from_wallet_error)?;
1044 Ok(Json(result))
1045}
1046
1047pub async fn discover_by_attributes(
1049 State(wallet): State<WalletState>,
1050 headers: HeaderMap,
1051 Json(args): Json<DiscoverByAttributesArgs>,
1052) -> Result<Json<DiscoverCertificatesResult>, AppError> {
1053 let originator = extract_originator(&headers)?;
1054 let result = wallet
1055 .discover_by_attributes(args, &originator)
1056 .await
1057 .map_err(AppError::from_wallet_error)?;
1058 Ok(Json(result))
1059}
1060
1061pub async fn reveal_counterparty_key_linkage(
1063 State(wallet): State<WalletState>,
1064 headers: HeaderMap,
1065 Json(req): Json<McRevealCounterpartyKeyLinkageReq>,
1066) -> Result<Json<RevealCounterpartyKeyLinkageResult>, AppError> {
1067 let originator = extract_originator(&headers)?;
1068 let args = WalletRevealCounterpartyArgs {
1069 counterparty: PublicKey::from_hex(&req.counterparty)
1070 .map_err(|e| anyhow::anyhow!("{}", e))?,
1071 verifier: PublicKey::from_hex(&req.verifier).map_err(|e| anyhow::anyhow!("{}", e))?,
1072 privileged: req.privileged,
1073 privileged_reason: req.privileged_reason,
1074 };
1075 let result = wallet
1076 .reveal_counterparty_key_linkage(args, &originator)
1077 .await
1078 .map_err(AppError::from_wallet_error)?;
1079 Ok(Json(result))
1080}
1081
1082pub async fn reveal_specific_key_linkage(
1084 State(wallet): State<WalletState>,
1085 headers: HeaderMap,
1086 Json(req): Json<McRevealSpecificKeyLinkageReq>,
1087) -> Result<Json<RevealSpecificKeyLinkageResult>, AppError> {
1088 let originator = extract_originator(&headers)?;
1089 let args = WalletRevealSpecificArgs {
1090 counterparty: parse_counterparty(&req.counterparty)?,
1091 verifier: PublicKey::from_hex(&req.verifier).map_err(|e| anyhow::anyhow!("{}", e))?,
1092 protocol_id: parse_protocol_id(&req.protocol_id)?,
1093 key_id: req.key_id,
1094 privileged: req.privileged,
1095 privileged_reason: req.privileged_reason,
1096 };
1097 let result = wallet
1098 .reveal_specific_key_linkage(args, &originator)
1099 .await
1100 .map_err(AppError::from_wallet_error)?;
1101 Ok(Json(result))
1102}