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 SignActionResult,
62 TrustSelf,
63 VerifyHmacArgs,
64 VerifySignatureArgs,
65 WalletCertificate,
66 WalletInterface,
67 WalletPayment,
68 WalletRevealCounterpartyArgs,
69 WalletRevealSpecificArgs,
70};
71use bsv_wallet_toolbox::{Services, StorageSqlx, Wallet};
72use serde_json::json;
73use std::sync::Arc;
74
75use super::types::*;
76
77pub type WalletState = Arc<Wallet<StorageSqlx, Services>>;
78
79use super::audit;
80
81fn cp_str(c: &Counterparty) -> String {
85 match c {
89 Counterparty::Self_ => "self".to_string(),
90 Counterparty::Anyone => "anyone".to_string(),
91 Counterparty::Other(pk) => pk.to_string().to_lowercase(),
92 }
93}
94
95fn level_num(l: SecurityLevel) -> u8 {
96 match l {
97 SecurityLevel::Silent => 0,
98 SecurityLevel::App => 1,
99 SecurityLevel::Counterparty => 2,
100 }
101}
102
103fn extract_originator(
105 headers: &HeaderMap,
106) -> Result<String, (StatusCode, Json<serde_json::Value>)> {
107 if let Some(origin) = headers.get("origin").and_then(|v| v.to_str().ok()) {
108 if let Ok(url) = url::Url::parse(origin) {
109 if let Some(host) = url.host_str() {
110 return Ok(host.to_string());
111 }
112 }
113 }
114 if let Some(originator) = headers.get("originator").and_then(|v| v.to_str().ok()) {
115 return Ok(originator.to_string());
116 }
117 Err((
118 StatusCode::BAD_REQUEST,
119 Json(json!({"message": "Origin header required"})),
120 ))
121}
122
123pub struct AppError {
125 status: StatusCode,
126 code: &'static str,
127 message: String,
128}
129
130impl AppError {
131 fn classify(msg: &str) -> (StatusCode, &'static str) {
133 let clean = msg.strip_prefix("wallet error: ").unwrap_or(msg);
135
136 if clean.starts_with("Insufficient funds:") || clean.starts_with("insufficient funds:") {
137 (StatusCode::PAYMENT_REQUIRED, "INSUFFICIENT_FUNDS")
138 } else if clean.starts_with("Authentication required")
139 || clean.starts_with("Invalid identity key:")
140 {
141 (StatusCode::UNAUTHORIZED, "AUTHENTICATION_REQUIRED")
142 } else if clean.starts_with("Access denied:") {
143 (StatusCode::FORBIDDEN, "ACCESS_DENIED")
144 } else if clean.starts_with("Entity not found:") || clean.starts_with("not found:") {
145 (StatusCode::NOT_FOUND, "NOT_FOUND")
146 } else if clean.starts_with("Duplicate entity:") || clean.starts_with("Invalid operation:")
147 {
148 (StatusCode::CONFLICT, "CONFLICT")
149 } else if clean.starts_with("Validation error:")
150 || clean.starts_with("Invalid argument:")
151 || clean.starts_with("Origin header required")
152 {
153 (StatusCode::BAD_REQUEST, "VALIDATION_ERROR")
154 } else if clean.starts_with("Storage error:")
155 || clean.starts_with("Database error:")
156 || clean.starts_with("Internal error:")
157 {
158 (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR")
159 } else if clean.starts_with("Service error:")
160 || clean.starts_with("Network error:")
161 || clean.starts_with("Broadcast failed:")
162 {
163 (StatusCode::BAD_GATEWAY, "SERVICE_ERROR")
164 } else {
165 (StatusCode::BAD_REQUEST, "ERROR")
166 }
167 }
168
169 fn from_wallet_error(e: impl std::fmt::Display) -> Self {
171 let message = e.to_string();
172 let (status, code) = Self::classify(&message);
173 Self {
174 status,
175 code,
176 message,
177 }
178 }
179
180 fn message(&self) -> &str {
181 &self.message
182 }
183
184 fn with_hint(mut self, hint: &str) -> Self {
186 self.message = format!("{} (hint: {})", self.message, hint);
187 self
188 }
189
190 fn broadcast_rejected(txid: &str) -> Self {
195 Self {
196 status: StatusCode::BAD_GATEWAY,
197 code: "BROADCAST_REJECTED",
198 message: format!(
199 "broadcast rejected: transaction {txid} is absent from BOTH the broadcaster \
200 it was submitted to AND an independent chain index, after the full probe \
201 window. The broadcaster dropped it — most likely error 465 \"fee too low\" \
202 on a deep unconfirmed BEEF. The funds were NOT sent."
203 ),
204 }
205 }
206}
207
208impl From<anyhow::Error> for AppError {
209 fn from(err: anyhow::Error) -> Self {
210 Self::from_wallet_error(err)
211 }
212}
213
214impl From<(StatusCode, Json<serde_json::Value>)> for AppError {
215 fn from((status, json): (StatusCode, Json<serde_json::Value>)) -> Self {
216 let message = json
217 .0
218 .get("message")
219 .and_then(|m| m.as_str())
220 .unwrap_or_default()
221 .to_string();
222 let (_, code) = Self::classify(&message);
223 Self {
224 status,
225 code,
226 message,
227 }
228 }
229}
230
231impl IntoResponse for AppError {
232 fn into_response(self) -> Response {
233 tracing::warn!(
234 status = self.status.as_u16(),
235 code = self.code,
236 message = %self.message,
237 "request error"
238 );
239 (
240 self.status,
241 Json(json!({"code": self.code, "message": self.message})),
242 )
243 .into_response()
244 }
245}
246
247pub async fn is_authenticated() -> Json<serde_json::Value> {
249 Json(json!({"authenticated": true}))
250}
251
252pub async fn get_public_key(
254 State(wallet): State<WalletState>,
255 headers: HeaderMap,
256 Json(req): Json<McGetPublicKeyReq>,
257) -> Result<Json<McGetPublicKeyRes>, AppError> {
258 let originator = extract_originator(&headers)?;
259
260 let args = GetPublicKeyArgs {
261 identity_key: req.identity_key,
262 protocol_id: req.protocol_id.map(|v| parse_protocol_id(&v)).transpose()?,
263 key_id: req.key_id,
264 counterparty: req
265 .counterparty
266 .map(|s| parse_counterparty(&s))
267 .transpose()?,
268 for_self: req.for_self,
269 };
270
271 if let Some(p) = args.protocol_id.as_ref() {
272 audit::record(audit::protocol_entry(
273 "getPublicKey",
274 level_num(p.security_level),
275 &p.protocol_name,
276 args.counterparty.as_ref().map(cp_str),
277 args.key_id.clone(),
278 &originator,
279 ));
280 }
281 let result = wallet
282 .get_public_key(args, &originator)
283 .await
284 .map_err(AppError::from_wallet_error)?;
285
286 Ok(Json(McGetPublicKeyRes {
287 public_key: result.public_key,
288 }))
289}
290
291pub async fn create_signature(
293 State(wallet): State<WalletState>,
294 headers: HeaderMap,
295 Json(req): Json<McCreateSignatureReq>,
296) -> Result<Json<McCreateSignatureRes>, AppError> {
297 let originator = extract_originator(&headers)?;
298
299 let hash_to_directly_sign = req
302 .hash_to_directly_sign
303 .map(|h| {
304 <[u8; 32]>::try_from(h.as_slice()).map_err(|_| {
305 AppError::from_wallet_error("Validation error: hashToDirectlySign must be 32 bytes")
306 })
307 })
308 .transpose()?;
309 if req.data.is_none() && hash_to_directly_sign.is_none() {
310 return Err(AppError::from_wallet_error(
311 "Validation error: one of data or hashToDirectlySign is required",
312 ));
313 }
314
315 let args = CreateSignatureArgs {
316 data: req.data,
317 hash_to_directly_sign,
318 protocol_id: parse_protocol_id(&req.protocol_id)?,
319 key_id: req.key_id,
320 counterparty: Some(parse_counterparty(&req.counterparty)?),
321 };
322
323 audit::record(audit::protocol_entry(
324 "createSignature",
325 level_num(args.protocol_id.security_level),
326 &args.protocol_id.protocol_name,
327 args.counterparty.as_ref().map(cp_str),
328 Some(args.key_id.clone()),
329 &originator,
330 ));
331 let result = wallet
332 .create_signature(args, &originator)
333 .await
334 .map_err(AppError::from_wallet_error)?;
335
336 Ok(Json(McCreateSignatureRes {
337 signature: result.signature,
338 }))
339}
340
341pub async fn create_action(
347 State(wallet): State<WalletState>,
348 axum::Extension(spending_lock): axum::Extension<super::SpendingLock>,
349 axum::Extension(verifier): axum::Extension<crate::broadcast_verify::BroadcastVerifier>,
350 headers: HeaderMap,
351 Json(req): Json<McCreateActionReq>,
352) -> Result<Json<McCreateActionRes>, AppError> {
353 let originator = extract_originator(&headers)?;
354
355 let (opt_no_send, opt_accept_delayed) = req
358 .options
359 .as_ref()
360 .map(|o| {
361 (
362 o.no_send.unwrap_or(false),
363 o.accept_delayed_broadcast.unwrap_or(false),
364 )
365 })
366 .unwrap_or((false, false));
367
368 let outputs = req
369 .outputs
370 .map(|outs| {
371 outs.into_iter()
372 .map(|o| {
373 Ok(CreateActionOutput {
374 locking_script: hex::decode(&o.locking_script)?,
375 satoshis: o.satoshis,
376 output_description: o.output_description,
377 basket: o.basket,
378 custom_instructions: o.custom_instructions,
379 tags: o.tags,
380 })
381 })
382 .collect::<Result<Vec<_>>>()
383 })
384 .transpose()?;
385
386 let inputs = match req.inputs {
392 Some(reqs) if !reqs.is_empty() => {
393 let mut mapped = Vec::with_capacity(reqs.len());
394 for i in reqs {
395 let outpoint = Outpoint::from_string(&i.outpoint)
396 .map_err(|e| anyhow::anyhow!("invalid input outpoint '{}': {e}", i.outpoint))?;
397 let unlocking_script = match i.unlocking_script {
398 Some(h) => Some(hex::decode(&h).context("invalid unlockingScript hex")?),
399 None => None,
400 };
401 mapped.push(CreateActionInput {
402 outpoint,
403 input_description: i.input_description.unwrap_or_else(|| "input".to_string()),
404 unlocking_script,
405 unlocking_script_length: i.unlocking_script_length,
406 sequence_number: i.sequence_number,
407 });
408 }
409 Some(mapped)
410 }
411 _ => Some(vec![]),
412 };
413
414 let args = CreateActionArgs {
415 description: req.description,
416 input_beef: req.input_beef,
417 inputs,
418 outputs,
419 lock_time: req.lock_time,
420 version: None,
421 labels: req.labels,
422 options: req.options.map(|o| CreateActionOptions {
423 accept_delayed_broadcast: o.accept_delayed_broadcast,
424 randomize_outputs: o.randomize_outputs,
425 sign_and_process: o.sign_and_process,
426 no_send: o.no_send,
427 trust_self: match o.trust_self.as_deref() {
428 Some("known") => Some(TrustSelf::Known),
429 _ => None,
430 },
431 ..Default::default()
432 }),
433 };
434
435 let output_count = args.outputs.as_ref().map(|o| o.len()).unwrap_or(0);
436 let total_sats: u64 = args
437 .outputs
438 .as_ref()
439 .map(|o| o.iter().map(|x| x.satoshis).sum())
440 .unwrap_or(0);
441
442 audit::record(audit::spend_entry(
446 "createAction",
447 total_sats,
448 Some(args.description.clone()),
449 &originator,
450 ));
451
452 let _guard = spending_lock.lock().await;
455 let result = wallet
456 .create_action(args, &originator)
457 .await
458 .map_err(AppError::from_wallet_error)?;
459 drop(_guard);
460
461 tracing::info!(
462 originator = %originator,
463 outputs = output_count,
464 total_satoshis = total_sats,
465 txid = ?result.txid.as_ref().map(|t| to_hex(t)),
466 "createAction"
467 );
468
469 let atomic_beef = match (result.beef.as_ref(), result.txid.as_ref()) {
472 (Some(beef_bytes), Some(txid_bytes)) => {
473 let txid_hex = to_hex(txid_bytes);
474 match bsv_sdk::transaction::Beef::from_binary(beef_bytes) {
475 Ok(mut beef) => match beef.to_binary_atomic(&txid_hex) {
476 Ok(ab) => Some(ab),
477 Err(e) => {
478 tracing::warn!(error = %e, "Failed to build AtomicBEEF, falling back to raw tx");
479 result.tx.clone()
480 }
481 },
482 Err(e) => {
483 tracing::warn!(error = %e, "Failed to parse BEEF, falling back to raw tx");
484 result.tx.clone()
485 }
486 }
487 }
488 _ => result.tx.clone(),
489 };
490
491 if !opt_no_send && !opt_accept_delayed && result.signable_transaction.is_none() {
498 if let Some(txid_bytes) = result.txid.as_ref() {
499 let txid_hex = to_hex(txid_bytes);
500 if verifier.verify(&txid_hex).await
501 == crate::broadcast_verify::BroadcastVerification::Rejected
502 {
503 return Err(AppError::broadcast_rejected(&txid_hex));
504 }
505 }
506 }
507
508 Ok(Json(McCreateActionRes {
509 txid: result.txid.map(|t| to_hex(&t)),
510 tx: atomic_beef,
511 send_with_results: result
512 .send_with_results
513 .and_then(|r| serde_json::to_value(r).ok()),
514 signable_transaction: result.signable_transaction.map(|st| {
515 let reference =
519 String::from_utf8(st.reference).unwrap_or_else(|e| hex::encode(e.into_bytes()));
520 McSignableTransaction {
521 tx: st.tx,
522 reference,
523 }
524 }),
525 no_send_change: result
526 .no_send_change
527 .and_then(|v| serde_json::to_value(v).ok()),
528 }))
529}
530
531pub async fn internalize_action(
533 State(wallet): State<WalletState>,
534 headers: HeaderMap,
535 Json(req): Json<McInternalizeActionReq>,
536) -> Result<Json<McInternalizeActionRes>, AppError> {
537 let originator = extract_originator(&headers)?;
538
539 let args = InternalizeActionArgs {
540 tx: req.tx,
541 outputs: req
542 .outputs
543 .into_iter()
544 .map(|o| InternalizeOutput {
545 output_index: o.output_index,
546 protocol: o.protocol,
547 payment_remittance: o.payment_remittance.map(|p| WalletPayment {
548 derivation_prefix: p.derivation_prefix,
549 derivation_suffix: p.derivation_suffix,
550 sender_identity_key: p.sender_identity_key,
551 }),
552 insertion_remittance: o.insertion_remittance.map(|i| BasketInsertion {
553 basket: i.basket,
554 custom_instructions: i.custom_instructions,
555 tags: i.tags,
556 }),
557 })
558 .collect(),
559 description: req.description,
560 labels: req.labels,
561 seek_permission: None,
562 };
563
564 let output_count = args.outputs.len();
565 let result = wallet
566 .internalize_action(args, &originator)
567 .await
568 .map_err(AppError::from_wallet_error)?;
569
570 tracing::info!(
571 originator = %originator,
572 outputs = output_count,
573 accepted = result.accepted,
574 "internalizeAction"
575 );
576
577 Ok(Json(McInternalizeActionRes {
578 accepted: result.accepted,
579 }))
580}
581
582fn parse_protocol_id(v: &serde_json::Value) -> Result<Protocol> {
586 let arr = v.as_array().context("protocolID must be an array")?;
587 let level = arr
588 .first()
589 .and_then(|v| v.as_u64())
590 .context("protocolID[0] must be an integer")?;
591 let name = arr
592 .get(1)
593 .and_then(|v| v.as_str())
594 .context("protocolID[1] must be a string")?;
595 Ok(Protocol::new(
596 match level {
597 0 => SecurityLevel::Silent,
598 1 => SecurityLevel::App,
599 2 => SecurityLevel::Counterparty,
600 _ => anyhow::bail!("invalid security level: {level}"),
601 },
602 name,
603 ))
604}
605
606fn parse_counterparty(s: &str) -> Result<Counterparty> {
608 match s {
609 "self" => Ok(Counterparty::Self_),
610 "anyone" => Ok(Counterparty::Anyone),
611 hex => Ok(Counterparty::Other(PublicKey::from_hex(hex)?)),
612 }
613}
614
615pub async fn get_height(
621 State(wallet): State<WalletState>,
622) -> Result<Json<GetHeightResult>, AppError> {
623 let result = wallet
624 .get_height("localhost")
625 .await
626 .map_err(AppError::from_wallet_error)?;
627 Ok(Json(result))
628}
629
630pub async fn get_network(
632 State(wallet): State<WalletState>,
633) -> Result<Json<GetNetworkResult>, AppError> {
634 let result = wallet
635 .get_network("localhost")
636 .await
637 .map_err(AppError::from_wallet_error)?;
638 Ok(Json(result))
639}
640
641pub async fn get_version(
643 State(wallet): State<WalletState>,
644) -> Result<Json<GetVersionResult>, AppError> {
645 let result = wallet
646 .get_version("localhost")
647 .await
648 .map_err(AppError::from_wallet_error)?;
649 Ok(Json(result))
650}
651
652pub async fn wait_for_authentication(
654 State(wallet): State<WalletState>,
655) -> Result<Json<AuthenticatedResult>, AppError> {
656 let result = wallet
657 .wait_for_authentication("localhost")
658 .await
659 .map_err(AppError::from_wallet_error)?;
660 Ok(Json(result))
661}
662
663pub async fn get_header_for_height(
669 State(wallet): State<WalletState>,
670 headers: HeaderMap,
671 Json(args): Json<GetHeaderArgs>,
672) -> Result<Json<GetHeaderResult>, AppError> {
673 let originator = extract_originator(&headers)?;
674 let result = wallet
675 .get_header_for_height(args, &originator)
676 .await
677 .map_err(AppError::from_wallet_error)?;
678 Ok(Json(result))
679}
680
681pub async fn verify_signature(
687 State(wallet): State<WalletState>,
688 headers: HeaderMap,
689 Json(req): Json<McVerifySignatureReq>,
690) -> Result<Json<serde_json::Value>, AppError> {
691 let originator = extract_originator(&headers)?;
692 let args = VerifySignatureArgs {
693 data: req.data,
694 hash_to_directly_verify: None,
695 signature: req.signature,
696 protocol_id: parse_protocol_id(&req.protocol_id)?,
697 key_id: req.key_id,
698 counterparty: req
699 .counterparty
700 .map(|s| parse_counterparty(&s))
701 .transpose()?,
702 for_self: req.for_self,
703 };
704 let result = wallet
705 .verify_signature(args, &originator)
706 .await
707 .map_err(AppError::from_wallet_error)?;
708 Ok(Json(json!({ "valid": result.valid })))
709}
710
711pub async fn encrypt(
713 State(wallet): State<WalletState>,
714 headers: HeaderMap,
715 Json(req): Json<McEncryptReq>,
716) -> Result<Json<serde_json::Value>, AppError> {
717 let originator = extract_originator(&headers)?;
718 let args = EncryptArgs {
719 plaintext: req.plaintext,
720 protocol_id: parse_protocol_id(&req.protocol_id)?,
721 key_id: req.key_id,
722 counterparty: req
723 .counterparty
724 .map(|s| parse_counterparty(&s))
725 .transpose()?,
726 };
727 audit::record(audit::protocol_entry(
728 "encrypt",
729 level_num(args.protocol_id.security_level),
730 &args.protocol_id.protocol_name,
731 args.counterparty.as_ref().map(cp_str),
732 Some(args.key_id.clone()),
733 &originator,
734 ));
735 let result = wallet
736 .encrypt(args, &originator)
737 .await
738 .map_err(AppError::from_wallet_error)?;
739 Ok(Json(json!({ "ciphertext": result.ciphertext })))
740}
741
742pub async fn decrypt(
744 State(wallet): State<WalletState>,
745 headers: HeaderMap,
746 Json(req): Json<McDecryptReq>,
747) -> Result<Json<serde_json::Value>, AppError> {
748 let originator = extract_originator(&headers)?;
749 let args = DecryptArgs {
750 ciphertext: req.ciphertext,
751 protocol_id: parse_protocol_id(&req.protocol_id)?,
752 key_id: req.key_id,
753 counterparty: req
754 .counterparty
755 .map(|s| parse_counterparty(&s))
756 .transpose()?,
757 };
758 audit::record(audit::protocol_entry(
759 "decrypt",
760 level_num(args.protocol_id.security_level),
761 &args.protocol_id.protocol_name,
762 args.counterparty.as_ref().map(cp_str),
763 Some(args.key_id.clone()),
764 &originator,
765 ));
766 let result = wallet
767 .decrypt(args, &originator)
768 .await
769 .map_err(AppError::from_wallet_error)?;
770 Ok(Json(json!({ "plaintext": result.plaintext })))
771}
772
773pub async fn create_hmac(
775 State(wallet): State<WalletState>,
776 headers: HeaderMap,
777 Json(req): Json<McCreateHmacReq>,
778) -> Result<Json<serde_json::Value>, AppError> {
779 let originator = extract_originator(&headers)?;
780 let args = CreateHmacArgs {
781 data: req.data,
782 protocol_id: parse_protocol_id(&req.protocol_id)?,
783 key_id: req.key_id,
784 counterparty: req
785 .counterparty
786 .map(|s| parse_counterparty(&s))
787 .transpose()?,
788 };
789 audit::record(audit::protocol_entry(
790 "createHmac",
791 level_num(args.protocol_id.security_level),
792 &args.protocol_id.protocol_name,
793 args.counterparty.as_ref().map(cp_str),
794 Some(args.key_id.clone()),
795 &originator,
796 ));
797 let result = wallet
798 .create_hmac(args, &originator)
799 .await
800 .map_err(AppError::from_wallet_error)?;
801 Ok(Json(json!({ "hmac": result.hmac.to_vec() })))
802}
803
804pub async fn verify_hmac(
806 State(wallet): State<WalletState>,
807 headers: HeaderMap,
808 Json(req): Json<McVerifyHmacReq>,
809) -> Result<Json<serde_json::Value>, AppError> {
810 let originator = extract_originator(&headers)?;
811 let hmac: [u8; 32] = req
812 .hmac
813 .try_into()
814 .map_err(|_| anyhow::anyhow!("hmac must be exactly 32 bytes"))?;
815 let args = VerifyHmacArgs {
816 data: req.data,
817 hmac,
818 protocol_id: parse_protocol_id(&req.protocol_id)?,
819 key_id: req.key_id,
820 counterparty: req
821 .counterparty
822 .map(|s| parse_counterparty(&s))
823 .transpose()?,
824 };
825 let result = wallet
826 .verify_hmac(args, &originator)
827 .await
828 .map_err(AppError::from_wallet_error)?;
829 Ok(Json(json!({ "valid": result.valid })))
830}
831
832pub async fn sign_action(
838 State(wallet): State<WalletState>,
839 headers: HeaderMap,
840 Json(args): Json<SignActionArgs>,
841) -> Result<Json<McSignActionRes>, AppError> {
842 let originator = extract_originator(&headers)?;
843 let reference = args.reference.clone();
844 let result = wallet
845 .sign_action(args, &originator)
846 .await
847 .map_err(AppError::from_wallet_error)?;
848 tracing::info!(originator = %originator, reference = %reference, "signAction");
849 Ok(Json(McSignActionRes {
853 txid: result.txid.map(|t| to_hex(&t)),
854 tx: result.tx,
855 send_with_results: result
856 .send_with_results
857 .and_then(|r| serde_json::to_value(r).ok()),
858 }))
859}
860
861pub async fn abort_action(
863 State(wallet): State<WalletState>,
864 headers: HeaderMap,
865 Json(args): Json<AbortActionArgs>,
866) -> Result<Json<AbortActionResult>, AppError> {
867 let originator = extract_originator(&headers)?;
868 let result = wallet.abort_action(args, &originator).await.map_err(|e| {
869 let mut err = AppError::from_wallet_error(e);
874 if err.message().contains("unproven") {
875 err = err.with_hint(
876 "an 'unproven' tx may already be on-chain; if its broadcast \
877 never landed, run `bsv-wallet cleanup-abandoned --execute` \
878 (verifies chain absence before restoring inputs) or wait for \
879 the daemon's auto-reconcile tick",
880 );
881 }
882 err
883 })?;
884 Ok(Json(result))
885}
886
887pub async fn list_actions(
889 State(wallet): State<WalletState>,
890 headers: HeaderMap,
891 Json(args): Json<ListActionsArgs>,
892) -> Result<Json<ListActionsResult>, AppError> {
893 let originator = extract_originator(&headers)?;
894 let result = wallet
895 .list_actions(args, &originator)
896 .await
897 .map_err(AppError::from_wallet_error)?;
898 Ok(Json(result))
899}
900
901pub async fn list_outputs(
903 State(wallet): State<WalletState>,
904 headers: HeaderMap,
905 Json(args): Json<ListOutputsArgs>,
906) -> Result<Json<ListOutputsResult>, AppError> {
907 let originator = extract_originator(&headers)?;
908 let result = wallet
909 .list_outputs(args, &originator)
910 .await
911 .map_err(AppError::from_wallet_error)?;
912 Ok(Json(result))
913}
914
915pub async fn relinquish_output(
920 State(wallet): State<WalletState>,
921 axum::Extension(spending_lock): axum::Extension<super::SpendingLock>,
922 headers: HeaderMap,
923 Json(args): Json<RelinquishOutputArgs>,
924) -> Result<Json<RelinquishOutputResult>, AppError> {
925 let originator = extract_originator(&headers)?;
926 let _guard = spending_lock.lock().await;
927 let result = wallet
928 .relinquish_output(args, &originator)
929 .await
930 .map_err(AppError::from_wallet_error)?;
931 Ok(Json(result))
932}
933
934pub async fn acquire_certificate(
940 State(wallet): State<WalletState>,
941 headers: HeaderMap,
942 Json(args): Json<AcquireCertificateArgs>,
943) -> Result<Json<WalletCertificate>, AppError> {
944 let originator = extract_originator(&headers)?;
945 let result = wallet
946 .acquire_certificate(args, &originator)
947 .await
948 .map_err(AppError::from_wallet_error)?;
949 Ok(Json(result))
950}
951
952pub async fn list_certificates(
954 State(wallet): State<WalletState>,
955 headers: HeaderMap,
956 Json(args): Json<ListCertificatesArgs>,
957) -> Result<Json<ListCertificatesResult>, AppError> {
958 let originator = extract_originator(&headers)?;
959 let result = wallet
960 .list_certificates(args, &originator)
961 .await
962 .map_err(AppError::from_wallet_error)?;
963 Ok(Json(result))
964}
965
966pub async fn prove_certificate(
968 State(wallet): State<WalletState>,
969 headers: HeaderMap,
970 Json(args): Json<ProveCertificateArgs>,
971) -> Result<Json<ProveCertificateResult>, AppError> {
972 let originator = extract_originator(&headers)?;
973 let result = wallet
974 .prove_certificate(args, &originator)
975 .await
976 .map_err(AppError::from_wallet_error)?;
977 Ok(Json(result))
978}
979
980pub async fn relinquish_certificate(
982 State(wallet): State<WalletState>,
983 headers: HeaderMap,
984 Json(args): Json<RelinquishCertificateArgs>,
985) -> Result<Json<RelinquishCertificateResult>, AppError> {
986 let originator = extract_originator(&headers)?;
987 let result = wallet
988 .relinquish_certificate(args, &originator)
989 .await
990 .map_err(AppError::from_wallet_error)?;
991 Ok(Json(result))
992}
993
994pub async fn discover_by_identity_key(
1000 State(wallet): State<WalletState>,
1001 headers: HeaderMap,
1002 Json(args): Json<DiscoverByIdentityKeyArgs>,
1003) -> Result<Json<DiscoverCertificatesResult>, AppError> {
1004 let originator = extract_originator(&headers)?;
1005 let result = wallet
1006 .discover_by_identity_key(args, &originator)
1007 .await
1008 .map_err(AppError::from_wallet_error)?;
1009 Ok(Json(result))
1010}
1011
1012pub async fn discover_by_attributes(
1014 State(wallet): State<WalletState>,
1015 headers: HeaderMap,
1016 Json(args): Json<DiscoverByAttributesArgs>,
1017) -> Result<Json<DiscoverCertificatesResult>, AppError> {
1018 let originator = extract_originator(&headers)?;
1019 let result = wallet
1020 .discover_by_attributes(args, &originator)
1021 .await
1022 .map_err(AppError::from_wallet_error)?;
1023 Ok(Json(result))
1024}
1025
1026pub async fn reveal_counterparty_key_linkage(
1028 State(wallet): State<WalletState>,
1029 headers: HeaderMap,
1030 Json(req): Json<McRevealCounterpartyKeyLinkageReq>,
1031) -> Result<Json<RevealCounterpartyKeyLinkageResult>, AppError> {
1032 let originator = extract_originator(&headers)?;
1033 let args = WalletRevealCounterpartyArgs {
1034 counterparty: PublicKey::from_hex(&req.counterparty)
1035 .map_err(|e| anyhow::anyhow!("{}", e))?,
1036 verifier: PublicKey::from_hex(&req.verifier).map_err(|e| anyhow::anyhow!("{}", e))?,
1037 privileged: req.privileged,
1038 privileged_reason: req.privileged_reason,
1039 };
1040 let result = wallet
1041 .reveal_counterparty_key_linkage(args, &originator)
1042 .await
1043 .map_err(AppError::from_wallet_error)?;
1044 Ok(Json(result))
1045}
1046
1047pub async fn reveal_specific_key_linkage(
1049 State(wallet): State<WalletState>,
1050 headers: HeaderMap,
1051 Json(req): Json<McRevealSpecificKeyLinkageReq>,
1052) -> Result<Json<RevealSpecificKeyLinkageResult>, AppError> {
1053 let originator = extract_originator(&headers)?;
1054 let args = WalletRevealSpecificArgs {
1055 counterparty: parse_counterparty(&req.counterparty)?,
1056 verifier: PublicKey::from_hex(&req.verifier).map_err(|e| anyhow::anyhow!("{}", e))?,
1057 protocol_id: parse_protocol_id(&req.protocol_id)?,
1058 key_id: req.key_id,
1059 privileged: req.privileged,
1060 privileged_reason: req.privileged_reason,
1061 };
1062 let result = wallet
1063 .reveal_specific_key_linkage(args, &originator)
1064 .await
1065 .map_err(AppError::from_wallet_error)?;
1066 Ok(Json(result))
1067}