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("Service error:")
163 || clean.starts_with("Network error:")
164 || clean.starts_with("Broadcast failed:")
165 {
166 (StatusCode::BAD_GATEWAY, "SERVICE_ERROR")
167 } else {
168 (StatusCode::BAD_REQUEST, "ERROR")
169 }
170 }
171
172 fn from_wallet_error(e: impl std::fmt::Display) -> Self {
174 let message = e.to_string();
175 let (status, code) = Self::classify(&message);
176 Self {
177 status,
178 code,
179 message,
180 }
181 }
182
183 fn message(&self) -> &str {
184 &self.message
185 }
186
187 fn with_hint(mut self, hint: &str) -> Self {
189 self.message = format!("{} (hint: {})", self.message, hint);
190 self
191 }
192
193 fn broadcast_rejected(txid: &str) -> Self {
200 Self {
201 status: StatusCode::BAD_GATEWAY,
202 code: "BROADCAST_REJECTED",
203 message: format!(
204 "broadcast rejected: no broadcaster accepted transaction {txid}, and it is \
205 absent from the broadcaster it was submitted to and from an independent chain \
206 index after the full probe window; this wallet retired it (failed). An input \
207 it spent is released only where the chain vouches it unspent; any other stays \
208 locked and is re-checked."
209 ),
210 }
211 }
212}
213
214impl From<anyhow::Error> for AppError {
215 fn from(err: anyhow::Error) -> Self {
216 Self::from_wallet_error(err)
217 }
218}
219
220impl From<(StatusCode, Json<serde_json::Value>)> for AppError {
221 fn from((status, json): (StatusCode, Json<serde_json::Value>)) -> Self {
222 let message = json
223 .0
224 .get("message")
225 .and_then(|m| m.as_str())
226 .unwrap_or_default()
227 .to_string();
228 let (_, code) = Self::classify(&message);
229 Self {
230 status,
231 code,
232 message,
233 }
234 }
235}
236
237impl IntoResponse for AppError {
238 fn into_response(self) -> Response {
239 tracing::warn!(
240 status = self.status.as_u16(),
241 code = self.code,
242 message = %self.message,
243 "request error"
244 );
245 (
246 self.status,
247 Json(json!({"code": self.code, "message": self.message})),
248 )
249 .into_response()
250 }
251}
252
253pub async fn is_authenticated() -> Json<serde_json::Value> {
255 Json(json!({"authenticated": true}))
256}
257
258pub async fn get_public_key(
260 State(wallet): State<WalletState>,
261 headers: HeaderMap,
262 Json(req): Json<McGetPublicKeyReq>,
263) -> Result<Json<McGetPublicKeyRes>, AppError> {
264 let originator = extract_originator(&headers)?;
265
266 let args = GetPublicKeyArgs {
267 identity_key: req.identity_key,
268 protocol_id: req.protocol_id.map(|v| parse_protocol_id(&v)).transpose()?,
269 key_id: req.key_id,
270 counterparty: req
271 .counterparty
272 .map(|s| parse_counterparty(&s))
273 .transpose()?,
274 for_self: req.for_self,
275 };
276
277 if let Some(p) = args.protocol_id.as_ref() {
278 audit::record(audit::protocol_entry(
279 "getPublicKey",
280 level_num(p.security_level),
281 &p.protocol_name,
282 args.counterparty.as_ref().map(cp_str),
283 args.key_id.clone(),
284 &originator,
285 ));
286 }
287 let result = wallet
288 .get_public_key(args, &originator)
289 .await
290 .map_err(AppError::from_wallet_error)?;
291
292 Ok(Json(McGetPublicKeyRes {
293 public_key: result.public_key,
294 }))
295}
296
297pub async fn create_signature(
299 State(wallet): State<WalletState>,
300 headers: HeaderMap,
301 Json(req): Json<McCreateSignatureReq>,
302) -> Result<Json<McCreateSignatureRes>, AppError> {
303 let originator = extract_originator(&headers)?;
304
305 let hash_to_directly_sign = req
308 .hash_to_directly_sign
309 .map(|h| {
310 <[u8; 32]>::try_from(h.as_slice()).map_err(|_| {
311 AppError::from_wallet_error("Validation error: hashToDirectlySign must be 32 bytes")
312 })
313 })
314 .transpose()?;
315 if req.data.is_none() && hash_to_directly_sign.is_none() {
316 return Err(AppError::from_wallet_error(
317 "Validation error: one of data or hashToDirectlySign is required",
318 ));
319 }
320
321 let args = CreateSignatureArgs {
322 data: req.data,
323 hash_to_directly_sign,
324 protocol_id: parse_protocol_id(&req.protocol_id)?,
325 key_id: req.key_id,
326 counterparty: Some(parse_counterparty(&req.counterparty)?),
327 };
328
329 audit::record(audit::protocol_entry(
330 "createSignature",
331 level_num(args.protocol_id.security_level),
332 &args.protocol_id.protocol_name,
333 args.counterparty.as_ref().map(cp_str),
334 Some(args.key_id.clone()),
335 &originator,
336 ));
337 let result = wallet
338 .create_signature(args, &originator)
339 .await
340 .map_err(AppError::from_wallet_error)?;
341
342 Ok(Json(McCreateSignatureRes {
343 signature: result.signature,
344 }))
345}
346
347pub async fn create_action(
353 State(wallet): State<WalletState>,
354 axum::Extension(spending_lock): axum::Extension<super::SpendingLock>,
355 axum::Extension(verifier): axum::Extension<crate::broadcast_verify::BroadcastVerifier>,
356 headers: HeaderMap,
357 Json(req): Json<McCreateActionReq>,
358) -> Result<Json<McCreateActionRes>, AppError> {
359 let originator = extract_originator(&headers)?;
360
361 let (opt_no_send, opt_accept_delayed) = req
364 .options
365 .as_ref()
366 .map(|o| {
367 (
368 o.no_send.unwrap_or(false),
369 o.accept_delayed_broadcast.unwrap_or(false),
370 )
371 })
372 .unwrap_or((false, false));
373
374 let outputs = req
375 .outputs
376 .map(|outs| {
377 outs.into_iter()
378 .map(|o| {
379 Ok(CreateActionOutput {
380 locking_script: hex::decode(&o.locking_script)?,
381 satoshis: o.satoshis,
382 output_description: o.output_description,
383 basket: o.basket,
384 custom_instructions: o.custom_instructions,
385 tags: o.tags,
386 })
387 })
388 .collect::<Result<Vec<_>>>()
389 })
390 .transpose()?;
391
392 let inputs = match req.inputs {
398 Some(reqs) if !reqs.is_empty() => {
399 let mut mapped = Vec::with_capacity(reqs.len());
400 for i in reqs {
401 let outpoint = Outpoint::from_string(&i.outpoint)
402 .map_err(|e| anyhow::anyhow!("invalid input outpoint '{}': {e}", i.outpoint))?;
403 let unlocking_script = match i.unlocking_script {
404 Some(h) => Some(hex::decode(&h).context("invalid unlockingScript hex")?),
405 None => None,
406 };
407 mapped.push(CreateActionInput {
408 outpoint,
409 input_description: i.input_description.unwrap_or_else(|| "input".to_string()),
410 unlocking_script,
411 unlocking_script_length: i.unlocking_script_length,
412 sequence_number: i.sequence_number,
413 });
414 }
415 Some(mapped)
416 }
417 _ => Some(vec![]),
418 };
419
420 let args = CreateActionArgs {
421 description: req.description,
422 input_beef: req.input_beef,
423 inputs,
424 outputs,
425 lock_time: req.lock_time,
426 version: None,
427 labels: req.labels,
428 options: req.options.map(|o| CreateActionOptions {
429 accept_delayed_broadcast: o.accept_delayed_broadcast,
430 randomize_outputs: o.randomize_outputs,
431 sign_and_process: o.sign_and_process,
432 no_send: o.no_send,
433 trust_self: match o.trust_self.as_deref() {
434 Some("known") => Some(TrustSelf::Known),
435 _ => None,
436 },
437 ..Default::default()
438 }),
439 };
440
441 let output_count = args.outputs.as_ref().map(|o| o.len()).unwrap_or(0);
442 let total_sats: u64 = args
443 .outputs
444 .as_ref()
445 .map(|o| o.iter().map(|x| x.satoshis).sum())
446 .unwrap_or(0);
447
448 audit::record(audit::spend_entry(
452 "createAction",
453 total_sats,
454 Some(args.description.clone()),
455 &originator,
456 ));
457
458 let _guard = spending_lock.lock().await;
461 crate::spend_guard::run(wallet.storage().pool())
464 .await
465 .map_err(|e| AppError::from_wallet_error(format!("Storage error: spend guard: {e}")))?;
466 let result = wallet
467 .create_action(args, &originator)
468 .await
469 .map_err(AppError::from_wallet_error)?;
470 drop(_guard);
471
472 tracing::info!(
473 originator = %originator,
474 outputs = output_count,
475 total_satoshis = total_sats,
476 txid = ?result.txid.as_ref().map(|t| to_hex(t)),
477 "createAction"
478 );
479
480 let atomic_beef = match (result.beef.as_ref(), result.txid.as_ref()) {
483 (Some(beef_bytes), Some(txid_bytes)) => {
484 let txid_hex = to_hex(txid_bytes);
485 match bsv_sdk::transaction::Beef::from_binary(beef_bytes) {
486 Ok(mut beef) => match beef.to_binary_atomic(&txid_hex) {
487 Ok(ab) => Some(ab),
488 Err(e) => {
489 tracing::warn!(error = %e, "Failed to build AtomicBEEF, falling back to raw tx");
490 result.tx.clone()
491 }
492 },
493 Err(e) => {
494 tracing::warn!(error = %e, "Failed to parse BEEF, falling back to raw tx");
495 result.tx.clone()
496 }
497 }
498 }
499 _ => result.tx.clone(),
500 };
501
502 let disposition =
516 super::broadcast_follow_up::disposition(&result, opt_no_send, opt_accept_delayed);
517 if let Some(txid_bytes) = result.txid.as_ref() {
518 let txid_hex = to_hex(txid_bytes);
519 let probe = verifier.clone();
520 let report_wallet = wallet.clone();
521 let ancestors: Vec<String> = result
524 .beef
525 .as_deref()
526 .map(|beef| bsv_wallet_toolbox::unproven_ancestors_in_beef(beef, &txid_hex))
527 .unwrap_or_default();
528 let outcome = super::broadcast_follow_up::follow_up(
529 disposition,
530 txid_hex.clone(),
531 move |txid| async move { probe.verify_report(&txid).await },
532 move |txid, report| async move {
533 super::broadcast_follow_up::apply_presence_report(
534 report_wallet.storage(),
535 report_wallet.services(),
536 &txid,
537 &ancestors,
538 &report,
539 )
540 .await;
541 },
542 )
543 .await;
544 if outcome == super::broadcast_follow_up::FollowUp::Rejected
545 && super::broadcast_follow_up::tx_status(wallet.storage(), &txid_hex).await
546 == Some("failed".to_string())
547 {
548 return Err(AppError::broadcast_rejected(&txid_hex));
549 }
550 }
551 let broadcast = match result.txid.as_ref() {
552 Some(t) if super::broadcast_follow_up::sending(&result.send_with_results, t) => {
553 super::post_word::post_word(wallet.storage(), &to_hex(t)).await
554 }
555 _ => None,
556 };
557
558 Ok(Json(McCreateActionRes {
559 txid: result.txid.map(|t| to_hex(&t)),
560 tx: atomic_beef,
561 send_with_results: result
562 .send_with_results
563 .and_then(|r| serde_json::to_value(r).ok()),
564 signable_transaction: result.signable_transaction.map(|st| {
565 let reference =
569 String::from_utf8(st.reference).unwrap_or_else(|e| hex::encode(e.into_bytes()));
570 McSignableTransaction {
571 tx: st.tx,
572 reference,
573 }
574 }),
575 no_send_change: result
576 .no_send_change
577 .and_then(|v| serde_json::to_value(v).ok()),
578 broadcast,
579 }))
580}
581
582pub async fn internalize_action(
584 State(wallet): State<WalletState>,
585 headers: HeaderMap,
586 Json(req): Json<McInternalizeActionReq>,
587) -> Result<Json<McInternalizeActionRes>, AppError> {
588 let originator = extract_originator(&headers)?;
589
590 let args = InternalizeActionArgs {
591 tx: req.tx,
592 outputs: req
593 .outputs
594 .into_iter()
595 .map(|o| InternalizeOutput {
596 output_index: o.output_index,
597 protocol: o.protocol,
598 payment_remittance: o.payment_remittance.map(|p| WalletPayment {
599 derivation_prefix: p.derivation_prefix,
600 derivation_suffix: p.derivation_suffix,
601 sender_identity_key: p.sender_identity_key,
602 }),
603 insertion_remittance: o.insertion_remittance.map(|i| BasketInsertion {
604 basket: i.basket,
605 custom_instructions: i.custom_instructions,
606 tags: i.tags,
607 }),
608 })
609 .collect(),
610 description: req.description,
611 labels: req.labels,
612 seek_permission: None,
613 };
614
615 let output_count = args.outputs.len();
616 let subject = super::post_word::atomic_subject(&args.tx);
617 let result = wallet
618 .internalize_action(args, &originator)
619 .await
620 .map_err(AppError::from_wallet_error)?;
621
622 tracing::info!(
623 originator = %originator,
624 outputs = output_count,
625 accepted = result.accepted,
626 "internalizeAction"
627 );
628 let broadcast = match subject {
631 Some(txid) => super::post_word::post_word(wallet.storage(), &txid).await,
632 None => None,
633 };
634
635 Ok(Json(McInternalizeActionRes {
636 accepted: result.accepted,
637 broadcast,
638 }))
639}
640
641fn parse_protocol_id(v: &serde_json::Value) -> Result<Protocol> {
645 let arr = v.as_array().context("protocolID must be an array")?;
646 let level = arr
647 .first()
648 .and_then(|v| v.as_u64())
649 .context("protocolID[0] must be an integer")?;
650 let name = arr
651 .get(1)
652 .and_then(|v| v.as_str())
653 .context("protocolID[1] must be a string")?;
654 Ok(Protocol::new(
655 match level {
656 0 => SecurityLevel::Silent,
657 1 => SecurityLevel::App,
658 2 => SecurityLevel::Counterparty,
659 _ => anyhow::bail!("invalid security level: {level}"),
660 },
661 name,
662 ))
663}
664
665fn parse_counterparty(s: &str) -> Result<Counterparty> {
667 match s {
668 "self" => Ok(Counterparty::Self_),
669 "anyone" => Ok(Counterparty::Anyone),
670 hex => Ok(Counterparty::Other(PublicKey::from_hex(hex)?)),
671 }
672}
673
674pub async fn get_height(
680 State(wallet): State<WalletState>,
681) -> Result<Json<GetHeightResult>, AppError> {
682 let result = wallet
683 .get_height("localhost")
684 .await
685 .map_err(AppError::from_wallet_error)?;
686 Ok(Json(result))
687}
688
689pub async fn get_network(
691 State(wallet): State<WalletState>,
692) -> Result<Json<GetNetworkResult>, AppError> {
693 let result = wallet
694 .get_network("localhost")
695 .await
696 .map_err(AppError::from_wallet_error)?;
697 Ok(Json(result))
698}
699
700pub async fn get_version(
702 State(wallet): State<WalletState>,
703) -> Result<Json<GetVersionResult>, AppError> {
704 let result = wallet
705 .get_version("localhost")
706 .await
707 .map_err(AppError::from_wallet_error)?;
708 Ok(Json(result))
709}
710
711pub async fn wait_for_authentication(
713 State(wallet): State<WalletState>,
714) -> Result<Json<AuthenticatedResult>, AppError> {
715 let result = wallet
716 .wait_for_authentication("localhost")
717 .await
718 .map_err(AppError::from_wallet_error)?;
719 Ok(Json(result))
720}
721
722pub async fn get_header_for_height(
728 State(wallet): State<WalletState>,
729 headers: HeaderMap,
730 Json(args): Json<GetHeaderArgs>,
731) -> Result<Json<GetHeaderResult>, AppError> {
732 let originator = extract_originator(&headers)?;
733 let result = wallet
734 .get_header_for_height(args, &originator)
735 .await
736 .map_err(AppError::from_wallet_error)?;
737 Ok(Json(result))
738}
739
740pub async fn verify_signature(
746 State(wallet): State<WalletState>,
747 headers: HeaderMap,
748 Json(req): Json<McVerifySignatureReq>,
749) -> Result<Json<serde_json::Value>, AppError> {
750 let originator = extract_originator(&headers)?;
751 let args = VerifySignatureArgs {
752 data: req.data,
753 hash_to_directly_verify: None,
754 signature: req.signature,
755 protocol_id: parse_protocol_id(&req.protocol_id)?,
756 key_id: req.key_id,
757 counterparty: req
758 .counterparty
759 .map(|s| parse_counterparty(&s))
760 .transpose()?,
761 for_self: req.for_self,
762 };
763 let result = wallet
764 .verify_signature(args, &originator)
765 .await
766 .map_err(AppError::from_wallet_error)?;
767 Ok(Json(json!({ "valid": result.valid })))
768}
769
770pub async fn encrypt(
772 State(wallet): State<WalletState>,
773 headers: HeaderMap,
774 Json(req): Json<McEncryptReq>,
775) -> Result<Json<serde_json::Value>, AppError> {
776 let originator = extract_originator(&headers)?;
777 let args = EncryptArgs {
778 plaintext: req.plaintext,
779 protocol_id: parse_protocol_id(&req.protocol_id)?,
780 key_id: req.key_id,
781 counterparty: req
782 .counterparty
783 .map(|s| parse_counterparty(&s))
784 .transpose()?,
785 };
786 audit::record(audit::protocol_entry(
787 "encrypt",
788 level_num(args.protocol_id.security_level),
789 &args.protocol_id.protocol_name,
790 args.counterparty.as_ref().map(cp_str),
791 Some(args.key_id.clone()),
792 &originator,
793 ));
794 let result = wallet
795 .encrypt(args, &originator)
796 .await
797 .map_err(AppError::from_wallet_error)?;
798 Ok(Json(json!({ "ciphertext": result.ciphertext })))
799}
800
801pub async fn decrypt(
803 State(wallet): State<WalletState>,
804 headers: HeaderMap,
805 Json(req): Json<McDecryptReq>,
806) -> Result<Json<serde_json::Value>, AppError> {
807 let originator = extract_originator(&headers)?;
808 let args = DecryptArgs {
809 ciphertext: req.ciphertext,
810 protocol_id: parse_protocol_id(&req.protocol_id)?,
811 key_id: req.key_id,
812 counterparty: req
813 .counterparty
814 .map(|s| parse_counterparty(&s))
815 .transpose()?,
816 };
817 audit::record(audit::protocol_entry(
818 "decrypt",
819 level_num(args.protocol_id.security_level),
820 &args.protocol_id.protocol_name,
821 args.counterparty.as_ref().map(cp_str),
822 Some(args.key_id.clone()),
823 &originator,
824 ));
825 let result = wallet
826 .decrypt(args, &originator)
827 .await
828 .map_err(AppError::from_wallet_error)?;
829 Ok(Json(json!({ "plaintext": result.plaintext })))
830}
831
832pub async fn create_hmac(
834 State(wallet): State<WalletState>,
835 headers: HeaderMap,
836 Json(req): Json<McCreateHmacReq>,
837) -> Result<Json<serde_json::Value>, AppError> {
838 let originator = extract_originator(&headers)?;
839 let args = CreateHmacArgs {
840 data: req.data,
841 protocol_id: parse_protocol_id(&req.protocol_id)?,
842 key_id: req.key_id,
843 counterparty: req
844 .counterparty
845 .map(|s| parse_counterparty(&s))
846 .transpose()?,
847 };
848 audit::record(audit::protocol_entry(
849 "createHmac",
850 level_num(args.protocol_id.security_level),
851 &args.protocol_id.protocol_name,
852 args.counterparty.as_ref().map(cp_str),
853 Some(args.key_id.clone()),
854 &originator,
855 ));
856 let result = wallet
857 .create_hmac(args, &originator)
858 .await
859 .map_err(AppError::from_wallet_error)?;
860 Ok(Json(json!({ "hmac": result.hmac.to_vec() })))
861}
862
863pub async fn verify_hmac(
865 State(wallet): State<WalletState>,
866 headers: HeaderMap,
867 Json(req): Json<McVerifyHmacReq>,
868) -> Result<Json<serde_json::Value>, AppError> {
869 let originator = extract_originator(&headers)?;
870 let hmac: [u8; 32] = req
871 .hmac
872 .try_into()
873 .map_err(|_| anyhow::anyhow!("hmac must be exactly 32 bytes"))?;
874 let args = VerifyHmacArgs {
875 data: req.data,
876 hmac,
877 protocol_id: parse_protocol_id(&req.protocol_id)?,
878 key_id: req.key_id,
879 counterparty: req
880 .counterparty
881 .map(|s| parse_counterparty(&s))
882 .transpose()?,
883 };
884 let result = wallet
885 .verify_hmac(args, &originator)
886 .await
887 .map_err(AppError::from_wallet_error)?;
888 Ok(Json(json!({ "valid": result.valid })))
889}
890
891pub async fn sign_action(
897 State(wallet): State<WalletState>,
898 headers: HeaderMap,
899 Json(args): Json<SignActionArgs>,
900) -> Result<Json<McSignActionRes>, AppError> {
901 let originator = extract_originator(&headers)?;
902 let reference = args.reference.clone();
903 let result = wallet
904 .sign_action(args, &originator)
905 .await
906 .map_err(AppError::from_wallet_error)?;
907 tracing::info!(originator = %originator, reference = %reference, "signAction");
908 let broadcast = match result.txid.as_ref() {
909 Some(t) if super::broadcast_follow_up::sending(&result.send_with_results, t) => {
910 super::post_word::post_word(wallet.storage(), &to_hex(t)).await
911 }
912 _ => None,
913 };
914 Ok(Json(McSignActionRes {
918 txid: result.txid.map(|t| to_hex(&t)),
919 tx: result.tx,
920 send_with_results: result
921 .send_with_results
922 .and_then(|r| serde_json::to_value(r).ok()),
923 broadcast,
924 }))
925}
926
927pub async fn abort_action(
929 State(wallet): State<WalletState>,
930 headers: HeaderMap,
931 Json(args): Json<AbortActionArgs>,
932) -> Result<Json<AbortActionResult>, AppError> {
933 let originator = extract_originator(&headers)?;
934 if let Some(txid) =
939 crate::retire_guard::abort_refused_for(wallet.storage().pool(), &args.reference)
940 .await
941 .map_err(AppError::from_wallet_error)?
942 {
943 return Err(AppError::from_wallet_error(format!(
944 "Invalid operation: a broadcaster took transaction {txid}; it is not aborted. It stays, \
945 re-asked, until a proof, a node verdict or a competitor's checked proof decides it"
946 )));
947 }
948 let result = wallet.abort_action(args, &originator).await.map_err(|e| {
949 let mut err = AppError::from_wallet_error(e);
956 if err.message().contains("the network has transaction") {
957 err = err.with_hint(
958 "the wallet holds chain evidence for this transaction, so it \
959 cannot be aborted; if it is a phantom after all, \
960 `bsv-wallet reconcile-broadcasts --execute` re-checks it \
961 against the chain index",
962 );
963 }
964 err
965 })?;
966 Ok(Json(result))
967}
968
969pub async fn list_actions(
971 State(wallet): State<WalletState>,
972 headers: HeaderMap,
973 Json(args): Json<ListActionsArgs>,
974) -> Result<Json<ListActionsResult>, AppError> {
975 let originator = extract_originator(&headers)?;
976 let result = wallet
977 .list_actions(args, &originator)
978 .await
979 .map_err(AppError::from_wallet_error)?;
980 Ok(Json(result))
981}
982
983pub async fn list_outputs(
985 State(wallet): State<WalletState>,
986 headers: HeaderMap,
987 Json(args): Json<ListOutputsArgs>,
988) -> Result<Json<ListOutputsResult>, AppError> {
989 let originator = extract_originator(&headers)?;
990 let result = wallet
991 .list_outputs(args, &originator)
992 .await
993 .map_err(AppError::from_wallet_error)?;
994 Ok(Json(result))
995}
996
997pub async fn relinquish_output(
1002 State(wallet): State<WalletState>,
1003 axum::Extension(spending_lock): axum::Extension<super::SpendingLock>,
1004 headers: HeaderMap,
1005 Json(args): Json<RelinquishOutputArgs>,
1006) -> Result<Json<RelinquishOutputResult>, AppError> {
1007 let originator = extract_originator(&headers)?;
1008 let _guard = spending_lock.lock().await;
1009 let result = wallet
1010 .relinquish_output(args, &originator)
1011 .await
1012 .map_err(AppError::from_wallet_error)?;
1013 Ok(Json(result))
1014}
1015
1016pub async fn acquire_certificate(
1022 State(wallet): State<WalletState>,
1023 headers: HeaderMap,
1024 Json(args): Json<AcquireCertificateArgs>,
1025) -> Result<Json<WalletCertificate>, AppError> {
1026 let originator = extract_originator(&headers)?;
1027 let result = wallet
1028 .acquire_certificate(args, &originator)
1029 .await
1030 .map_err(AppError::from_wallet_error)?;
1031 Ok(Json(result))
1032}
1033
1034pub async fn list_certificates(
1036 State(wallet): State<WalletState>,
1037 headers: HeaderMap,
1038 Json(args): Json<ListCertificatesArgs>,
1039) -> Result<Json<ListCertificatesResult>, AppError> {
1040 let originator = extract_originator(&headers)?;
1041 let result = wallet
1042 .list_certificates(args, &originator)
1043 .await
1044 .map_err(AppError::from_wallet_error)?;
1045 Ok(Json(result))
1046}
1047
1048pub async fn prove_certificate(
1050 State(wallet): State<WalletState>,
1051 headers: HeaderMap,
1052 Json(args): Json<ProveCertificateArgs>,
1053) -> Result<Json<ProveCertificateResult>, AppError> {
1054 let originator = extract_originator(&headers)?;
1055 let result = wallet
1056 .prove_certificate(args, &originator)
1057 .await
1058 .map_err(AppError::from_wallet_error)?;
1059 Ok(Json(result))
1060}
1061
1062pub async fn relinquish_certificate(
1064 State(wallet): State<WalletState>,
1065 headers: HeaderMap,
1066 Json(args): Json<RelinquishCertificateArgs>,
1067) -> Result<Json<RelinquishCertificateResult>, AppError> {
1068 let originator = extract_originator(&headers)?;
1069 let result = wallet
1070 .relinquish_certificate(args, &originator)
1071 .await
1072 .map_err(AppError::from_wallet_error)?;
1073 Ok(Json(result))
1074}
1075
1076pub async fn discover_by_identity_key(
1082 State(wallet): State<WalletState>,
1083 headers: HeaderMap,
1084 Json(args): Json<DiscoverByIdentityKeyArgs>,
1085) -> Result<Json<DiscoverCertificatesResult>, AppError> {
1086 let originator = extract_originator(&headers)?;
1087 let result = wallet
1088 .discover_by_identity_key(args, &originator)
1089 .await
1090 .map_err(AppError::from_wallet_error)?;
1091 Ok(Json(result))
1092}
1093
1094pub async fn discover_by_attributes(
1096 State(wallet): State<WalletState>,
1097 headers: HeaderMap,
1098 Json(args): Json<DiscoverByAttributesArgs>,
1099) -> Result<Json<DiscoverCertificatesResult>, AppError> {
1100 let originator = extract_originator(&headers)?;
1101 let result = wallet
1102 .discover_by_attributes(args, &originator)
1103 .await
1104 .map_err(AppError::from_wallet_error)?;
1105 Ok(Json(result))
1106}
1107
1108pub async fn reveal_counterparty_key_linkage(
1110 State(wallet): State<WalletState>,
1111 headers: HeaderMap,
1112 Json(req): Json<McRevealCounterpartyKeyLinkageReq>,
1113) -> Result<Json<RevealCounterpartyKeyLinkageResult>, AppError> {
1114 let originator = extract_originator(&headers)?;
1115 let args = WalletRevealCounterpartyArgs {
1116 counterparty: PublicKey::from_hex(&req.counterparty)
1117 .map_err(|e| anyhow::anyhow!("{}", e))?,
1118 verifier: PublicKey::from_hex(&req.verifier).map_err(|e| anyhow::anyhow!("{}", e))?,
1119 privileged: req.privileged,
1120 privileged_reason: req.privileged_reason,
1121 };
1122 let result = wallet
1123 .reveal_counterparty_key_linkage(args, &originator)
1124 .await
1125 .map_err(AppError::from_wallet_error)?;
1126 Ok(Json(result))
1127}
1128
1129pub async fn reveal_specific_key_linkage(
1131 State(wallet): State<WalletState>,
1132 headers: HeaderMap,
1133 Json(req): Json<McRevealSpecificKeyLinkageReq>,
1134) -> Result<Json<RevealSpecificKeyLinkageResult>, AppError> {
1135 let originator = extract_originator(&headers)?;
1136 let args = WalletRevealSpecificArgs {
1137 counterparty: parse_counterparty(&req.counterparty)?,
1138 verifier: PublicKey::from_hex(&req.verifier).map_err(|e| anyhow::anyhow!("{}", e))?,
1139 protocol_id: parse_protocol_id(&req.protocol_id)?,
1140 key_id: req.key_id,
1141 privileged: req.privileged,
1142 privileged_reason: req.privileged_reason,
1143 };
1144 let result = wallet
1145 .reveal_specific_key_linkage(args, &originator)
1146 .await
1147 .map_err(AppError::from_wallet_error)?;
1148 Ok(Json(result))
1149}