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("Invalid BEEF at byte") {
149 (StatusCode::BAD_REQUEST, "INVALID_BEEF")
152 } else if clean.starts_with("Validation error:")
153 || clean.starts_with("Invalid argument:")
154 || clean.starts_with("Origin header required")
155 {
156 (StatusCode::BAD_REQUEST, "VALIDATION_ERROR")
157 } else if clean.starts_with("Storage error:")
158 || clean.starts_with("Database error:")
159 || clean.starts_with("Internal error:")
160 {
161 (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR")
162 } else if clean.starts_with("Transaction broadcast failed") {
163 (StatusCode::BAD_GATEWAY, "BROADCAST_REJECTED")
168 } else if clean.starts_with("Service error:")
169 || clean.starts_with("Network error:")
170 || clean.starts_with("Broadcast failed:")
171 {
172 (StatusCode::BAD_GATEWAY, "SERVICE_ERROR")
173 } else {
174 (StatusCode::BAD_REQUEST, "ERROR")
175 }
176 }
177
178 fn from_wallet_error(e: impl std::fmt::Display) -> Self {
180 let message = e.to_string();
181 let (status, code) = Self::classify(&message);
182 Self {
183 status,
184 code,
185 message,
186 }
187 }
188
189 fn message(&self) -> &str {
190 &self.message
191 }
192
193 fn with_hint(mut self, hint: &str) -> Self {
195 self.message = format!("{} (hint: {})", self.message, hint);
196 self
197 }
198
199 fn broadcast_rejected(txid: &str) -> Self {
204 Self {
205 status: StatusCode::BAD_GATEWAY,
206 code: "BROADCAST_REJECTED",
207 message: format!(
208 "broadcast rejected: transaction {txid} is absent from BOTH the broadcaster \
209 it was submitted to AND an independent chain index, after the full probe \
210 window. The broadcaster dropped it — most likely error 465 \"fee too low\" \
211 on a deep unconfirmed BEEF. The funds were NOT sent."
212 ),
213 }
214 }
215}
216
217impl From<anyhow::Error> for AppError {
218 fn from(err: anyhow::Error) -> Self {
219 Self::from_wallet_error(err)
220 }
221}
222
223impl From<(StatusCode, Json<serde_json::Value>)> for AppError {
224 fn from((status, json): (StatusCode, Json<serde_json::Value>)) -> Self {
225 let message = json
226 .0
227 .get("message")
228 .and_then(|m| m.as_str())
229 .unwrap_or_default()
230 .to_string();
231 let (_, code) = Self::classify(&message);
232 Self {
233 status,
234 code,
235 message,
236 }
237 }
238}
239
240impl IntoResponse for AppError {
241 fn into_response(self) -> Response {
242 tracing::warn!(
243 status = self.status.as_u16(),
244 code = self.code,
245 message = %self.message,
246 "request error"
247 );
248 (
249 self.status,
250 Json(json!({"code": self.code, "message": self.message})),
251 )
252 .into_response()
253 }
254}
255
256pub async fn is_authenticated() -> Json<serde_json::Value> {
258 Json(json!({"authenticated": true}))
259}
260
261pub async fn get_public_key(
263 State(wallet): State<WalletState>,
264 headers: HeaderMap,
265 Json(req): Json<McGetPublicKeyReq>,
266) -> Result<Json<McGetPublicKeyRes>, AppError> {
267 let originator = extract_originator(&headers)?;
268
269 let args = GetPublicKeyArgs {
270 identity_key: req.identity_key,
271 protocol_id: req.protocol_id.map(|v| parse_protocol_id(&v)).transpose()?,
272 key_id: req.key_id,
273 counterparty: req
274 .counterparty
275 .map(|s| parse_counterparty(&s))
276 .transpose()?,
277 for_self: req.for_self,
278 };
279
280 if let Some(p) = args.protocol_id.as_ref() {
281 audit::record(audit::protocol_entry(
282 "getPublicKey",
283 level_num(p.security_level),
284 &p.protocol_name,
285 args.counterparty.as_ref().map(cp_str),
286 args.key_id.clone(),
287 &originator,
288 ));
289 }
290 let result = wallet
291 .get_public_key(args, &originator)
292 .await
293 .map_err(AppError::from_wallet_error)?;
294
295 Ok(Json(McGetPublicKeyRes {
296 public_key: result.public_key,
297 }))
298}
299
300pub async fn create_signature(
302 State(wallet): State<WalletState>,
303 headers: HeaderMap,
304 Json(req): Json<McCreateSignatureReq>,
305) -> Result<Json<McCreateSignatureRes>, AppError> {
306 let originator = extract_originator(&headers)?;
307
308 let hash_to_directly_sign = req
311 .hash_to_directly_sign
312 .map(|h| {
313 <[u8; 32]>::try_from(h.as_slice()).map_err(|_| {
314 AppError::from_wallet_error("Validation error: hashToDirectlySign must be 32 bytes")
315 })
316 })
317 .transpose()?;
318 if req.data.is_none() && hash_to_directly_sign.is_none() {
319 return Err(AppError::from_wallet_error(
320 "Validation error: one of data or hashToDirectlySign is required",
321 ));
322 }
323
324 let args = CreateSignatureArgs {
325 data: req.data,
326 hash_to_directly_sign,
327 protocol_id: parse_protocol_id(&req.protocol_id)?,
328 key_id: req.key_id,
329 counterparty: Some(parse_counterparty(&req.counterparty)?),
330 };
331
332 audit::record(audit::protocol_entry(
333 "createSignature",
334 level_num(args.protocol_id.security_level),
335 &args.protocol_id.protocol_name,
336 args.counterparty.as_ref().map(cp_str),
337 Some(args.key_id.clone()),
338 &originator,
339 ));
340 let result = wallet
341 .create_signature(args, &originator)
342 .await
343 .map_err(AppError::from_wallet_error)?;
344
345 Ok(Json(McCreateSignatureRes {
346 signature: result.signature,
347 }))
348}
349
350pub async fn create_action(
356 State(wallet): State<WalletState>,
357 axum::Extension(spending_lock): axum::Extension<super::SpendingLock>,
358 axum::Extension(verifier): axum::Extension<crate::broadcast_verify::BroadcastVerifier>,
359 headers: HeaderMap,
360 Json(req): Json<McCreateActionReq>,
361) -> Result<Json<McCreateActionRes>, AppError> {
362 let originator = extract_originator(&headers)?;
363
364 let (opt_no_send, opt_accept_delayed) = req
367 .options
368 .as_ref()
369 .map(|o| {
370 (
371 o.no_send.unwrap_or(false),
372 o.accept_delayed_broadcast.unwrap_or(false),
373 )
374 })
375 .unwrap_or((false, false));
376
377 let outputs = req
378 .outputs
379 .map(|outs| {
380 outs.into_iter()
381 .map(|o| {
382 Ok(CreateActionOutput {
383 locking_script: hex::decode(&o.locking_script)?,
384 satoshis: o.satoshis,
385 output_description: o.output_description,
386 basket: o.basket,
387 custom_instructions: o.custom_instructions,
388 tags: o.tags,
389 })
390 })
391 .collect::<Result<Vec<_>>>()
392 })
393 .transpose()?;
394
395 let inputs = match req.inputs {
401 Some(reqs) if !reqs.is_empty() => {
402 let mut mapped = Vec::with_capacity(reqs.len());
403 for i in reqs {
404 let outpoint = Outpoint::from_string(&i.outpoint)
405 .map_err(|e| anyhow::anyhow!("invalid input outpoint '{}': {e}", i.outpoint))?;
406 let unlocking_script = match i.unlocking_script {
407 Some(h) => Some(hex::decode(&h).context("invalid unlockingScript hex")?),
408 None => None,
409 };
410 mapped.push(CreateActionInput {
411 outpoint,
412 input_description: i.input_description.unwrap_or_else(|| "input".to_string()),
413 unlocking_script,
414 unlocking_script_length: i.unlocking_script_length,
415 sequence_number: i.sequence_number,
416 });
417 }
418 Some(mapped)
419 }
420 _ => Some(vec![]),
421 };
422
423 let args = CreateActionArgs {
424 description: req.description,
425 input_beef: req.input_beef,
426 inputs,
427 outputs,
428 lock_time: req.lock_time,
429 version: None,
430 labels: req.labels,
431 options: req.options.map(|o| CreateActionOptions {
432 accept_delayed_broadcast: o.accept_delayed_broadcast,
433 randomize_outputs: o.randomize_outputs,
434 sign_and_process: o.sign_and_process,
435 no_send: o.no_send,
436 trust_self: match o.trust_self.as_deref() {
437 Some("known") => Some(TrustSelf::Known),
438 _ => None,
439 },
440 ..Default::default()
441 }),
442 };
443
444 let output_count = args.outputs.as_ref().map(|o| o.len()).unwrap_or(0);
445 let total_sats: u64 = args
446 .outputs
447 .as_ref()
448 .map(|o| o.iter().map(|x| x.satoshis).sum())
449 .unwrap_or(0);
450
451 audit::record(audit::spend_entry(
455 "createAction",
456 total_sats,
457 Some(args.description.clone()),
458 &originator,
459 ));
460
461 let _guard = spending_lock.lock().await;
464 let result = wallet
465 .create_action(args, &originator)
466 .await
467 .map_err(AppError::from_wallet_error)?;
468 drop(_guard);
469
470 tracing::info!(
471 originator = %originator,
472 outputs = output_count,
473 total_satoshis = total_sats,
474 txid = ?result.txid.as_ref().map(|t| to_hex(t)),
475 "createAction"
476 );
477
478 let atomic_beef = match (result.beef.as_ref(), result.txid.as_ref()) {
481 (Some(beef_bytes), Some(txid_bytes)) => {
482 let txid_hex = to_hex(txid_bytes);
483 match bsv_sdk::transaction::Beef::from_binary(beef_bytes) {
484 Ok(mut beef) => match beef.to_binary_atomic(&txid_hex) {
485 Ok(ab) => Some(ab),
486 Err(e) => {
487 tracing::warn!(error = %e, "Failed to build AtomicBEEF, falling back to raw tx");
488 result.tx.clone()
489 }
490 },
491 Err(e) => {
492 tracing::warn!(error = %e, "Failed to parse BEEF, falling back to raw tx");
493 result.tx.clone()
494 }
495 }
496 }
497 _ => result.tx.clone(),
498 };
499
500 let disposition =
512 super::broadcast_follow_up::disposition(&result, opt_no_send, opt_accept_delayed);
513 if let Some(txid_bytes) = result.txid.as_ref() {
514 let txid_hex = to_hex(txid_bytes);
515 let probe = verifier.clone();
516 let report_wallet = wallet.clone();
517 let ancestors: Vec<String> = result
520 .beef
521 .as_deref()
522 .map(|beef| bsv_wallet_toolbox::unproven_ancestors_in_beef(beef, &txid_hex))
523 .unwrap_or_default();
524 let outcome = super::broadcast_follow_up::follow_up(
525 disposition,
526 txid_hex.clone(),
527 move |txid| async move { probe.verify_report(&txid).await },
528 move |txid, report| async move {
529 super::broadcast_follow_up::apply_presence_report(
530 report_wallet.storage(),
531 report_wallet.services(),
532 &txid,
533 &ancestors,
534 &report,
535 )
536 .await;
537 },
538 )
539 .await;
540 if outcome == super::broadcast_follow_up::FollowUp::Rejected {
541 return Err(AppError::broadcast_rejected(&txid_hex));
542 }
543 }
544
545 Ok(Json(McCreateActionRes {
546 txid: result.txid.map(|t| to_hex(&t)),
547 tx: atomic_beef,
548 send_with_results: result
549 .send_with_results
550 .and_then(|r| serde_json::to_value(r).ok()),
551 signable_transaction: result.signable_transaction.map(|st| {
552 let reference =
556 String::from_utf8(st.reference).unwrap_or_else(|e| hex::encode(e.into_bytes()));
557 McSignableTransaction {
558 tx: st.tx,
559 reference,
560 }
561 }),
562 no_send_change: result
563 .no_send_change
564 .and_then(|v| serde_json::to_value(v).ok()),
565 }))
566}
567
568pub async fn internalize_action(
570 State(wallet): State<WalletState>,
571 headers: HeaderMap,
572 Json(req): Json<McInternalizeActionReq>,
573) -> Result<Json<McInternalizeActionRes>, AppError> {
574 let originator = extract_originator(&headers)?;
575
576 let args = InternalizeActionArgs {
577 tx: req.tx,
578 outputs: req
579 .outputs
580 .into_iter()
581 .map(|o| InternalizeOutput {
582 output_index: o.output_index,
583 protocol: o.protocol,
584 payment_remittance: o.payment_remittance.map(|p| WalletPayment {
585 derivation_prefix: p.derivation_prefix,
586 derivation_suffix: p.derivation_suffix,
587 sender_identity_key: p.sender_identity_key,
588 }),
589 insertion_remittance: o.insertion_remittance.map(|i| BasketInsertion {
590 basket: i.basket,
591 custom_instructions: i.custom_instructions,
592 tags: i.tags,
593 }),
594 })
595 .collect(),
596 description: req.description,
597 labels: req.labels,
598 seek_permission: None,
599 };
600
601 let output_count = args.outputs.len();
602 let result = wallet
603 .internalize_action(args, &originator)
604 .await
605 .map_err(AppError::from_wallet_error)?;
606
607 tracing::info!(
608 originator = %originator,
609 outputs = output_count,
610 accepted = result.accepted,
611 "internalizeAction"
612 );
613
614 Ok(Json(McInternalizeActionRes {
615 accepted: result.accepted,
616 }))
617}
618
619fn parse_protocol_id(v: &serde_json::Value) -> Result<Protocol> {
623 let arr = v.as_array().context("protocolID must be an array")?;
624 let level = arr
625 .first()
626 .and_then(|v| v.as_u64())
627 .context("protocolID[0] must be an integer")?;
628 let name = arr
629 .get(1)
630 .and_then(|v| v.as_str())
631 .context("protocolID[1] must be a string")?;
632 Ok(Protocol::new(
633 match level {
634 0 => SecurityLevel::Silent,
635 1 => SecurityLevel::App,
636 2 => SecurityLevel::Counterparty,
637 _ => anyhow::bail!("invalid security level: {level}"),
638 },
639 name,
640 ))
641}
642
643fn parse_counterparty(s: &str) -> Result<Counterparty> {
645 match s {
646 "self" => Ok(Counterparty::Self_),
647 "anyone" => Ok(Counterparty::Anyone),
648 hex => Ok(Counterparty::Other(PublicKey::from_hex(hex)?)),
649 }
650}
651
652pub async fn get_height(
658 State(wallet): State<WalletState>,
659) -> Result<Json<GetHeightResult>, AppError> {
660 let result = wallet
661 .get_height("localhost")
662 .await
663 .map_err(AppError::from_wallet_error)?;
664 Ok(Json(result))
665}
666
667pub async fn get_network(
669 State(wallet): State<WalletState>,
670) -> Result<Json<GetNetworkResult>, AppError> {
671 let result = wallet
672 .get_network("localhost")
673 .await
674 .map_err(AppError::from_wallet_error)?;
675 Ok(Json(result))
676}
677
678pub async fn get_version(
680 State(wallet): State<WalletState>,
681) -> Result<Json<GetVersionResult>, AppError> {
682 let result = wallet
683 .get_version("localhost")
684 .await
685 .map_err(AppError::from_wallet_error)?;
686 Ok(Json(result))
687}
688
689pub async fn wait_for_authentication(
691 State(wallet): State<WalletState>,
692) -> Result<Json<AuthenticatedResult>, AppError> {
693 let result = wallet
694 .wait_for_authentication("localhost")
695 .await
696 .map_err(AppError::from_wallet_error)?;
697 Ok(Json(result))
698}
699
700pub async fn get_header_for_height(
706 State(wallet): State<WalletState>,
707 headers: HeaderMap,
708 Json(args): Json<GetHeaderArgs>,
709) -> Result<Json<GetHeaderResult>, AppError> {
710 let originator = extract_originator(&headers)?;
711 let result = wallet
712 .get_header_for_height(args, &originator)
713 .await
714 .map_err(AppError::from_wallet_error)?;
715 Ok(Json(result))
716}
717
718pub async fn verify_signature(
724 State(wallet): State<WalletState>,
725 headers: HeaderMap,
726 Json(req): Json<McVerifySignatureReq>,
727) -> Result<Json<serde_json::Value>, AppError> {
728 let originator = extract_originator(&headers)?;
729 let args = VerifySignatureArgs {
730 data: req.data,
731 hash_to_directly_verify: None,
732 signature: req.signature,
733 protocol_id: parse_protocol_id(&req.protocol_id)?,
734 key_id: req.key_id,
735 counterparty: req
736 .counterparty
737 .map(|s| parse_counterparty(&s))
738 .transpose()?,
739 for_self: req.for_self,
740 };
741 let result = wallet
742 .verify_signature(args, &originator)
743 .await
744 .map_err(AppError::from_wallet_error)?;
745 Ok(Json(json!({ "valid": result.valid })))
746}
747
748pub async fn encrypt(
750 State(wallet): State<WalletState>,
751 headers: HeaderMap,
752 Json(req): Json<McEncryptReq>,
753) -> Result<Json<serde_json::Value>, AppError> {
754 let originator = extract_originator(&headers)?;
755 let args = EncryptArgs {
756 plaintext: req.plaintext,
757 protocol_id: parse_protocol_id(&req.protocol_id)?,
758 key_id: req.key_id,
759 counterparty: req
760 .counterparty
761 .map(|s| parse_counterparty(&s))
762 .transpose()?,
763 };
764 audit::record(audit::protocol_entry(
765 "encrypt",
766 level_num(args.protocol_id.security_level),
767 &args.protocol_id.protocol_name,
768 args.counterparty.as_ref().map(cp_str),
769 Some(args.key_id.clone()),
770 &originator,
771 ));
772 let result = wallet
773 .encrypt(args, &originator)
774 .await
775 .map_err(AppError::from_wallet_error)?;
776 Ok(Json(json!({ "ciphertext": result.ciphertext })))
777}
778
779pub async fn decrypt(
781 State(wallet): State<WalletState>,
782 headers: HeaderMap,
783 Json(req): Json<McDecryptReq>,
784) -> Result<Json<serde_json::Value>, AppError> {
785 let originator = extract_originator(&headers)?;
786 let args = DecryptArgs {
787 ciphertext: req.ciphertext,
788 protocol_id: parse_protocol_id(&req.protocol_id)?,
789 key_id: req.key_id,
790 counterparty: req
791 .counterparty
792 .map(|s| parse_counterparty(&s))
793 .transpose()?,
794 };
795 audit::record(audit::protocol_entry(
796 "decrypt",
797 level_num(args.protocol_id.security_level),
798 &args.protocol_id.protocol_name,
799 args.counterparty.as_ref().map(cp_str),
800 Some(args.key_id.clone()),
801 &originator,
802 ));
803 let result = wallet
804 .decrypt(args, &originator)
805 .await
806 .map_err(AppError::from_wallet_error)?;
807 Ok(Json(json!({ "plaintext": result.plaintext })))
808}
809
810pub async fn create_hmac(
812 State(wallet): State<WalletState>,
813 headers: HeaderMap,
814 Json(req): Json<McCreateHmacReq>,
815) -> Result<Json<serde_json::Value>, AppError> {
816 let originator = extract_originator(&headers)?;
817 let args = CreateHmacArgs {
818 data: req.data,
819 protocol_id: parse_protocol_id(&req.protocol_id)?,
820 key_id: req.key_id,
821 counterparty: req
822 .counterparty
823 .map(|s| parse_counterparty(&s))
824 .transpose()?,
825 };
826 audit::record(audit::protocol_entry(
827 "createHmac",
828 level_num(args.protocol_id.security_level),
829 &args.protocol_id.protocol_name,
830 args.counterparty.as_ref().map(cp_str),
831 Some(args.key_id.clone()),
832 &originator,
833 ));
834 let result = wallet
835 .create_hmac(args, &originator)
836 .await
837 .map_err(AppError::from_wallet_error)?;
838 Ok(Json(json!({ "hmac": result.hmac.to_vec() })))
839}
840
841pub async fn verify_hmac(
843 State(wallet): State<WalletState>,
844 headers: HeaderMap,
845 Json(req): Json<McVerifyHmacReq>,
846) -> Result<Json<serde_json::Value>, AppError> {
847 let originator = extract_originator(&headers)?;
848 let hmac: [u8; 32] = req
849 .hmac
850 .try_into()
851 .map_err(|_| anyhow::anyhow!("hmac must be exactly 32 bytes"))?;
852 let args = VerifyHmacArgs {
853 data: req.data,
854 hmac,
855 protocol_id: parse_protocol_id(&req.protocol_id)?,
856 key_id: req.key_id,
857 counterparty: req
858 .counterparty
859 .map(|s| parse_counterparty(&s))
860 .transpose()?,
861 };
862 let result = wallet
863 .verify_hmac(args, &originator)
864 .await
865 .map_err(AppError::from_wallet_error)?;
866 Ok(Json(json!({ "valid": result.valid })))
867}
868
869pub async fn sign_action(
875 State(wallet): State<WalletState>,
876 headers: HeaderMap,
877 Json(args): Json<SignActionArgs>,
878) -> Result<Json<McSignActionRes>, AppError> {
879 let originator = extract_originator(&headers)?;
880 let reference = args.reference.clone();
881 let result = wallet
882 .sign_action(args, &originator)
883 .await
884 .map_err(AppError::from_wallet_error)?;
885 tracing::info!(originator = %originator, reference = %reference, "signAction");
886 Ok(Json(McSignActionRes {
890 txid: result.txid.map(|t| to_hex(&t)),
891 tx: result.tx,
892 send_with_results: result
893 .send_with_results
894 .and_then(|r| serde_json::to_value(r).ok()),
895 }))
896}
897
898pub async fn abort_action(
900 State(wallet): State<WalletState>,
901 headers: HeaderMap,
902 Json(args): Json<AbortActionArgs>,
903) -> Result<Json<AbortActionResult>, AppError> {
904 let originator = extract_originator(&headers)?;
905 let result = wallet.abort_action(args, &originator).await.map_err(|e| {
906 let mut err = AppError::from_wallet_error(e);
913 if err.message().contains("the network has transaction") {
914 err = err.with_hint(
915 "the wallet holds chain evidence for this transaction, so it \
916 cannot be aborted; if it is a phantom after all, \
917 `bsv-wallet reconcile-broadcasts --execute` re-checks it \
918 against the chain index",
919 );
920 }
921 err
922 })?;
923 Ok(Json(result))
924}
925
926pub async fn list_actions(
928 State(wallet): State<WalletState>,
929 headers: HeaderMap,
930 Json(args): Json<ListActionsArgs>,
931) -> Result<Json<ListActionsResult>, AppError> {
932 let originator = extract_originator(&headers)?;
933 let result = wallet
934 .list_actions(args, &originator)
935 .await
936 .map_err(AppError::from_wallet_error)?;
937 Ok(Json(result))
938}
939
940pub async fn list_outputs(
942 State(wallet): State<WalletState>,
943 headers: HeaderMap,
944 Json(args): Json<ListOutputsArgs>,
945) -> Result<Json<ListOutputsResult>, AppError> {
946 let originator = extract_originator(&headers)?;
947 let result = wallet
948 .list_outputs(args, &originator)
949 .await
950 .map_err(AppError::from_wallet_error)?;
951 Ok(Json(result))
952}
953
954pub async fn relinquish_output(
959 State(wallet): State<WalletState>,
960 axum::Extension(spending_lock): axum::Extension<super::SpendingLock>,
961 headers: HeaderMap,
962 Json(args): Json<RelinquishOutputArgs>,
963) -> Result<Json<RelinquishOutputResult>, AppError> {
964 let originator = extract_originator(&headers)?;
965 let _guard = spending_lock.lock().await;
966 let result = wallet
967 .relinquish_output(args, &originator)
968 .await
969 .map_err(AppError::from_wallet_error)?;
970 Ok(Json(result))
971}
972
973pub async fn acquire_certificate(
979 State(wallet): State<WalletState>,
980 headers: HeaderMap,
981 Json(args): Json<AcquireCertificateArgs>,
982) -> Result<Json<WalletCertificate>, AppError> {
983 let originator = extract_originator(&headers)?;
984 let result = wallet
985 .acquire_certificate(args, &originator)
986 .await
987 .map_err(AppError::from_wallet_error)?;
988 Ok(Json(result))
989}
990
991pub async fn list_certificates(
993 State(wallet): State<WalletState>,
994 headers: HeaderMap,
995 Json(args): Json<ListCertificatesArgs>,
996) -> Result<Json<ListCertificatesResult>, AppError> {
997 let originator = extract_originator(&headers)?;
998 let result = wallet
999 .list_certificates(args, &originator)
1000 .await
1001 .map_err(AppError::from_wallet_error)?;
1002 Ok(Json(result))
1003}
1004
1005pub async fn prove_certificate(
1007 State(wallet): State<WalletState>,
1008 headers: HeaderMap,
1009 Json(args): Json<ProveCertificateArgs>,
1010) -> Result<Json<ProveCertificateResult>, AppError> {
1011 let originator = extract_originator(&headers)?;
1012 let result = wallet
1013 .prove_certificate(args, &originator)
1014 .await
1015 .map_err(AppError::from_wallet_error)?;
1016 Ok(Json(result))
1017}
1018
1019pub async fn relinquish_certificate(
1021 State(wallet): State<WalletState>,
1022 headers: HeaderMap,
1023 Json(args): Json<RelinquishCertificateArgs>,
1024) -> Result<Json<RelinquishCertificateResult>, AppError> {
1025 let originator = extract_originator(&headers)?;
1026 let result = wallet
1027 .relinquish_certificate(args, &originator)
1028 .await
1029 .map_err(AppError::from_wallet_error)?;
1030 Ok(Json(result))
1031}
1032
1033pub async fn discover_by_identity_key(
1039 State(wallet): State<WalletState>,
1040 headers: HeaderMap,
1041 Json(args): Json<DiscoverByIdentityKeyArgs>,
1042) -> Result<Json<DiscoverCertificatesResult>, AppError> {
1043 let originator = extract_originator(&headers)?;
1044 let result = wallet
1045 .discover_by_identity_key(args, &originator)
1046 .await
1047 .map_err(AppError::from_wallet_error)?;
1048 Ok(Json(result))
1049}
1050
1051pub async fn discover_by_attributes(
1053 State(wallet): State<WalletState>,
1054 headers: HeaderMap,
1055 Json(args): Json<DiscoverByAttributesArgs>,
1056) -> Result<Json<DiscoverCertificatesResult>, AppError> {
1057 let originator = extract_originator(&headers)?;
1058 let result = wallet
1059 .discover_by_attributes(args, &originator)
1060 .await
1061 .map_err(AppError::from_wallet_error)?;
1062 Ok(Json(result))
1063}
1064
1065pub async fn reveal_counterparty_key_linkage(
1067 State(wallet): State<WalletState>,
1068 headers: HeaderMap,
1069 Json(req): Json<McRevealCounterpartyKeyLinkageReq>,
1070) -> Result<Json<RevealCounterpartyKeyLinkageResult>, AppError> {
1071 let originator = extract_originator(&headers)?;
1072 let args = WalletRevealCounterpartyArgs {
1073 counterparty: PublicKey::from_hex(&req.counterparty)
1074 .map_err(|e| anyhow::anyhow!("{}", e))?,
1075 verifier: PublicKey::from_hex(&req.verifier).map_err(|e| anyhow::anyhow!("{}", e))?,
1076 privileged: req.privileged,
1077 privileged_reason: req.privileged_reason,
1078 };
1079 let result = wallet
1080 .reveal_counterparty_key_linkage(args, &originator)
1081 .await
1082 .map_err(AppError::from_wallet_error)?;
1083 Ok(Json(result))
1084}
1085
1086pub async fn reveal_specific_key_linkage(
1088 State(wallet): State<WalletState>,
1089 headers: HeaderMap,
1090 Json(req): Json<McRevealSpecificKeyLinkageReq>,
1091) -> Result<Json<RevealSpecificKeyLinkageResult>, AppError> {
1092 let originator = extract_originator(&headers)?;
1093 let args = WalletRevealSpecificArgs {
1094 counterparty: parse_counterparty(&req.counterparty)?,
1095 verifier: PublicKey::from_hex(&req.verifier).map_err(|e| anyhow::anyhow!("{}", e))?,
1096 protocol_id: parse_protocol_id(&req.protocol_id)?,
1097 key_id: req.key_id,
1098 privileged: req.privileged,
1099 privileged_reason: req.privileged_reason,
1100 };
1101 let result = wallet
1102 .reveal_specific_key_linkage(args, &originator)
1103 .await
1104 .map_err(AppError::from_wallet_error)?;
1105 Ok(Json(result))
1106}