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 refusal: Option<crate::spend_gate::Refusal>,
129}
130
131impl AppError {
132 fn classify(msg: &str) -> (StatusCode, &'static str) {
134 let clean = msg.strip_prefix("wallet error: ").unwrap_or(msg);
136
137 if clean.starts_with("Insufficient funds:") || clean.starts_with("insufficient funds:") {
138 (StatusCode::PAYMENT_REQUIRED, "INSUFFICIENT_FUNDS")
139 } else if clean.starts_with("Authentication required")
140 || clean.starts_with("Invalid identity key:")
141 {
142 (StatusCode::UNAUTHORIZED, "AUTHENTICATION_REQUIRED")
143 } else if clean.starts_with("Access denied:") {
144 (StatusCode::FORBIDDEN, "ACCESS_DENIED")
145 } else if clean.starts_with("Entity not found:") || clean.starts_with("not found:") {
146 (StatusCode::NOT_FOUND, "NOT_FOUND")
147 } else if clean.starts_with("Duplicate entity:") || clean.starts_with("Invalid operation:")
148 {
149 (StatusCode::CONFLICT, "CONFLICT")
150 } else if clean.starts_with("Invalid BEEF at byte") {
151 (StatusCode::BAD_REQUEST, "INVALID_BEEF")
154 } else if clean.starts_with("Validation error:")
155 || clean.starts_with("Invalid argument:")
156 || clean.starts_with("Origin header required")
157 {
158 (StatusCode::BAD_REQUEST, "VALIDATION_ERROR")
159 } else if clean.starts_with("Storage error:")
160 || clean.starts_with("Database error:")
161 || clean.starts_with("Internal error:")
162 {
163 (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR")
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 refusal: None,
183 }
184 }
185
186 fn from_gate_error(e: crate::spend_gate::GateError) -> Self {
188 match e {
189 crate::spend_gate::GateError::Refused(refusal) => Self {
190 status: StatusCode::PAYMENT_REQUIRED,
191 code: "INSUFFICIENT_FUNDS",
192 message: refusal.to_string(),
193 refusal: Some(refusal),
194 },
195 other => Self::from_wallet_error(other),
196 }
197 }
198
199 fn message(&self) -> &str {
200 &self.message
201 }
202
203 fn with_hint(mut self, hint: &str) -> Self {
205 self.message = format!("{} (hint: {})", self.message, hint);
206 self
207 }
208
209 fn broadcast_rejected(txid: &str) -> Self {
216 Self {
217 status: StatusCode::BAD_GATEWAY,
218 code: "BROADCAST_REJECTED",
219 message: format!(
220 "broadcast rejected: no broadcaster accepted transaction {txid}, and it is \
221 absent from the broadcaster it was submitted to and from an independent chain \
222 index after the full probe window; this wallet retired it (failed). An input \
223 it spent is released only where the chain vouches it unspent; any other stays \
224 locked and is re-checked."
225 ),
226 refusal: None,
227 }
228 }
229}
230
231impl From<anyhow::Error> for AppError {
232 fn from(err: anyhow::Error) -> Self {
233 Self::from_wallet_error(err)
234 }
235}
236
237impl From<(StatusCode, Json<serde_json::Value>)> for AppError {
238 fn from((status, json): (StatusCode, Json<serde_json::Value>)) -> Self {
239 let message = json
240 .0
241 .get("message")
242 .and_then(|m| m.as_str())
243 .unwrap_or_default()
244 .to_string();
245 let (_, code) = Self::classify(&message);
246 Self {
247 status,
248 code,
249 message,
250 refusal: None,
251 }
252 }
253}
254
255impl IntoResponse for AppError {
256 fn into_response(self) -> Response {
257 tracing::warn!(
258 status = self.status.as_u16(),
259 code = self.code,
260 message = %self.message,
261 "request error"
262 );
263 let mut body = json!({"code": self.code, "message": self.message});
264 if let Some(refusal) = &self.refusal {
265 body["offeredSatoshis"] = json!(refusal.offered_satoshis);
266 body["held"] = json!(refusal
267 .held
268 .iter()
269 .map(|c| json!({
270 "outpoint": c.outpoint,
271 "satoshis": c.satoshis,
272 "word": c.word,
273 "origin": c.origin,
274 "ageMinutes": c.age_minutes,
275 "reason": c.reason,
276 }))
277 .collect::<Vec<_>>());
278 }
279 (self.status, Json(body)).into_response()
280 }
281}
282
283pub async fn is_authenticated() -> Json<serde_json::Value> {
285 Json(json!({"authenticated": true}))
286}
287
288pub async fn get_public_key(
290 State(wallet): State<WalletState>,
291 headers: HeaderMap,
292 Json(req): Json<McGetPublicKeyReq>,
293) -> Result<Json<McGetPublicKeyRes>, AppError> {
294 let originator = extract_originator(&headers)?;
295
296 let args = GetPublicKeyArgs {
297 identity_key: req.identity_key,
298 protocol_id: req.protocol_id.map(|v| parse_protocol_id(&v)).transpose()?,
299 key_id: req.key_id,
300 counterparty: req
301 .counterparty
302 .map(|s| parse_counterparty(&s))
303 .transpose()?,
304 for_self: req.for_self,
305 };
306
307 if let Some(p) = args.protocol_id.as_ref() {
308 audit::record(audit::protocol_entry(
309 "getPublicKey",
310 level_num(p.security_level),
311 &p.protocol_name,
312 args.counterparty.as_ref().map(cp_str),
313 args.key_id.clone(),
314 &originator,
315 ));
316 }
317 let result = wallet
318 .get_public_key(args, &originator)
319 .await
320 .map_err(AppError::from_wallet_error)?;
321
322 Ok(Json(McGetPublicKeyRes {
323 public_key: result.public_key,
324 }))
325}
326
327pub async fn create_signature(
329 State(wallet): State<WalletState>,
330 headers: HeaderMap,
331 Json(req): Json<McCreateSignatureReq>,
332) -> Result<Json<McCreateSignatureRes>, AppError> {
333 let originator = extract_originator(&headers)?;
334
335 let hash_to_directly_sign = req
338 .hash_to_directly_sign
339 .map(|h| {
340 <[u8; 32]>::try_from(h.as_slice()).map_err(|_| {
341 AppError::from_wallet_error("Validation error: hashToDirectlySign must be 32 bytes")
342 })
343 })
344 .transpose()?;
345 if req.data.is_none() && hash_to_directly_sign.is_none() {
346 return Err(AppError::from_wallet_error(
347 "Validation error: one of data or hashToDirectlySign is required",
348 ));
349 }
350
351 let args = CreateSignatureArgs {
352 data: req.data,
353 hash_to_directly_sign,
354 protocol_id: parse_protocol_id(&req.protocol_id)?,
355 key_id: req.key_id,
356 counterparty: Some(parse_counterparty(&req.counterparty)?),
357 };
358
359 audit::record(audit::protocol_entry(
360 "createSignature",
361 level_num(args.protocol_id.security_level),
362 &args.protocol_id.protocol_name,
363 args.counterparty.as_ref().map(cp_str),
364 Some(args.key_id.clone()),
365 &originator,
366 ));
367 let result = wallet
368 .create_signature(args, &originator)
369 .await
370 .map_err(AppError::from_wallet_error)?;
371
372 Ok(Json(McCreateSignatureRes {
373 signature: result.signature,
374 }))
375}
376
377pub async fn create_action(
383 State(wallet): State<WalletState>,
384 axum::Extension(spending_lock): axum::Extension<super::SpendingLock>,
385 axum::Extension(verifier): axum::Extension<crate::broadcast_verify::BroadcastVerifier>,
386 headers: HeaderMap,
387 Json(req): Json<McCreateActionReq>,
388) -> Result<Json<McCreateActionRes>, AppError> {
389 let originator = extract_originator(&headers)?;
390
391 let (opt_no_send, opt_accept_delayed) = req
394 .options
395 .as_ref()
396 .map(|o| {
397 (
398 o.no_send.unwrap_or(false),
399 o.accept_delayed_broadcast.unwrap_or(false),
400 )
401 })
402 .unwrap_or((false, false));
403
404 let outputs = req
405 .outputs
406 .map(|outs| {
407 outs.into_iter()
408 .map(|o| {
409 Ok(CreateActionOutput {
410 locking_script: hex::decode(&o.locking_script)?,
411 satoshis: o.satoshis,
412 output_description: o.output_description,
413 basket: o.basket,
414 custom_instructions: o.custom_instructions,
415 tags: o.tags,
416 })
417 })
418 .collect::<Result<Vec<_>>>()
419 })
420 .transpose()?;
421
422 let inputs = match req.inputs {
428 Some(reqs) if !reqs.is_empty() => {
429 let mut mapped = Vec::with_capacity(reqs.len());
430 for i in reqs {
431 let outpoint = Outpoint::from_string(&i.outpoint)
432 .map_err(|e| anyhow::anyhow!("invalid input outpoint '{}': {e}", i.outpoint))?;
433 let unlocking_script = match i.unlocking_script {
434 Some(h) => Some(hex::decode(&h).context("invalid unlockingScript hex")?),
435 None => None,
436 };
437 mapped.push(CreateActionInput {
438 outpoint,
439 input_description: i.input_description.unwrap_or_else(|| "input".to_string()),
440 unlocking_script,
441 unlocking_script_length: i.unlocking_script_length,
442 sequence_number: i.sequence_number,
443 });
444 }
445 Some(mapped)
446 }
447 _ => Some(vec![]),
448 };
449
450 let args = CreateActionArgs {
451 description: req.description,
452 input_beef: req.input_beef,
453 inputs,
454 outputs,
455 lock_time: req.lock_time,
456 version: None,
457 labels: req.labels,
458 options: req.options.map(|o| CreateActionOptions {
459 accept_delayed_broadcast: o.accept_delayed_broadcast,
460 randomize_outputs: o.randomize_outputs,
461 sign_and_process: o.sign_and_process,
462 no_send: o.no_send,
463 trust_self: match o.trust_self.as_deref() {
464 Some("known") => Some(TrustSelf::Known),
465 _ => None,
466 },
467 ..Default::default()
468 }),
469 };
470
471 let output_count = args.outputs.as_ref().map(|o| o.len()).unwrap_or(0);
472 let total_sats: u64 = args
473 .outputs
474 .as_ref()
475 .map(|o| o.iter().map(|x| x.satoshis).sum())
476 .unwrap_or(0);
477
478 audit::record(audit::spend_entry(
482 "createAction",
483 total_sats,
484 Some(args.description.clone()),
485 &originator,
486 ));
487
488 let _guard = spending_lock.lock().await;
491 crate::spend_guard::run(wallet.storage().pool())
494 .await
495 .map_err(|e| AppError::from_wallet_error(format!("Storage error: spend guard: {e}")))?;
496 let result = crate::spend_gate::create_action(
500 &wallet,
501 args,
502 &originator,
503 &crate::spend_gate::GateOptions::from_env(),
504 )
505 .await
506 .map_err(AppError::from_gate_error)?;
507 drop(_guard);
508
509 tracing::info!(
510 originator = %originator,
511 outputs = output_count,
512 total_satoshis = total_sats,
513 txid = ?result.txid.as_ref().map(|t| to_hex(t)),
514 "createAction"
515 );
516
517 let atomic_beef = match (result.beef.as_ref(), result.txid.as_ref()) {
520 (Some(beef_bytes), Some(txid_bytes)) => {
521 let txid_hex = to_hex(txid_bytes);
522 match bsv_sdk::transaction::Beef::from_binary(beef_bytes) {
523 Ok(mut beef) => match beef.to_binary_atomic(&txid_hex) {
524 Ok(ab) => Some(ab),
525 Err(e) => {
526 tracing::warn!(error = %e, "Failed to build AtomicBEEF, falling back to raw tx");
527 result.tx.clone()
528 }
529 },
530 Err(e) => {
531 tracing::warn!(error = %e, "Failed to parse BEEF, falling back to raw tx");
532 result.tx.clone()
533 }
534 }
535 }
536 _ => result.tx.clone(),
537 };
538
539 let disposition =
553 super::broadcast_follow_up::disposition(&result, opt_no_send, opt_accept_delayed);
554 if let Some(txid_bytes) = result.txid.as_ref() {
555 let txid_hex = to_hex(txid_bytes);
556 let probe = verifier.clone();
557 let report_wallet = wallet.clone();
558 let ancestors: Vec<String> = result
561 .beef
562 .as_deref()
563 .map(|beef| bsv_wallet_toolbox::unproven_ancestors_in_beef(beef, &txid_hex))
564 .unwrap_or_default();
565 let outcome = super::broadcast_follow_up::follow_up(
566 disposition,
567 txid_hex.clone(),
568 move |txid| async move { probe.verify_report(&txid).await },
569 move |txid, report| async move {
570 super::broadcast_follow_up::apply_presence_report(
571 report_wallet.storage(),
572 report_wallet.services(),
573 &txid,
574 &ancestors,
575 &report,
576 )
577 .await;
578 },
579 )
580 .await;
581 if outcome == super::broadcast_follow_up::FollowUp::Rejected
582 && super::broadcast_follow_up::tx_status(wallet.storage(), &txid_hex).await
583 == Some("failed".to_string())
584 {
585 return Err(AppError::broadcast_rejected(&txid_hex));
586 }
587 }
588 let broadcast = match result.txid.as_ref() {
589 Some(t) if super::broadcast_follow_up::sending(&result.send_with_results, t) => {
590 super::post_word::post_word(wallet.storage(), &to_hex(t)).await
591 }
592 _ => None,
593 };
594
595 Ok(Json(McCreateActionRes {
596 txid: result.txid.map(|t| to_hex(&t)),
597 tx: atomic_beef,
598 send_with_results: result
599 .send_with_results
600 .and_then(|r| serde_json::to_value(r).ok()),
601 signable_transaction: result.signable_transaction.map(|st| {
602 let reference =
606 String::from_utf8(st.reference).unwrap_or_else(|e| hex::encode(e.into_bytes()));
607 McSignableTransaction {
608 tx: st.tx,
609 reference,
610 }
611 }),
612 no_send_change: result
613 .no_send_change
614 .and_then(|v| serde_json::to_value(v).ok()),
615 broadcast,
616 }))
617}
618
619pub async fn internalize_action(
621 State(wallet): State<WalletState>,
622 headers: HeaderMap,
623 Json(req): Json<McInternalizeActionReq>,
624) -> Result<Json<McInternalizeActionRes>, AppError> {
625 let originator = extract_originator(&headers)?;
626
627 let args = InternalizeActionArgs {
628 tx: req.tx,
629 outputs: req
630 .outputs
631 .into_iter()
632 .map(|o| InternalizeOutput {
633 output_index: o.output_index,
634 protocol: o.protocol,
635 payment_remittance: o.payment_remittance.map(|p| WalletPayment {
636 derivation_prefix: p.derivation_prefix,
637 derivation_suffix: p.derivation_suffix,
638 sender_identity_key: p.sender_identity_key,
639 }),
640 insertion_remittance: o.insertion_remittance.map(|i| BasketInsertion {
641 basket: i.basket,
642 custom_instructions: i.custom_instructions,
643 tags: i.tags,
644 }),
645 })
646 .collect(),
647 description: req.description,
648 labels: req.labels,
649 seek_permission: None,
650 };
651
652 let output_count = args.outputs.len();
653 let subject = super::post_word::atomic_subject(&args.tx);
654 let result = wallet
655 .internalize_action(args, &originator)
656 .await
657 .map_err(AppError::from_wallet_error)?;
658
659 tracing::info!(
660 originator = %originator,
661 outputs = output_count,
662 accepted = result.accepted,
663 "internalizeAction"
664 );
665 let broadcast = match subject {
668 Some(txid) => super::post_word::post_word(wallet.storage(), &txid).await,
669 None => None,
670 };
671
672 Ok(Json(McInternalizeActionRes {
673 accepted: result.accepted,
674 broadcast,
675 }))
676}
677
678fn parse_protocol_id(v: &serde_json::Value) -> Result<Protocol> {
682 let arr = v.as_array().context("protocolID must be an array")?;
683 let level = arr
684 .first()
685 .and_then(|v| v.as_u64())
686 .context("protocolID[0] must be an integer")?;
687 let name = arr
688 .get(1)
689 .and_then(|v| v.as_str())
690 .context("protocolID[1] must be a string")?;
691 Ok(Protocol::new(
692 match level {
693 0 => SecurityLevel::Silent,
694 1 => SecurityLevel::App,
695 2 => SecurityLevel::Counterparty,
696 _ => anyhow::bail!("invalid security level: {level}"),
697 },
698 name,
699 ))
700}
701
702fn parse_counterparty(s: &str) -> Result<Counterparty> {
704 match s {
705 "self" => Ok(Counterparty::Self_),
706 "anyone" => Ok(Counterparty::Anyone),
707 hex => Ok(Counterparty::Other(PublicKey::from_hex(hex)?)),
708 }
709}
710
711pub async fn get_height(
717 State(wallet): State<WalletState>,
718) -> Result<Json<GetHeightResult>, AppError> {
719 let result = wallet
720 .get_height("localhost")
721 .await
722 .map_err(AppError::from_wallet_error)?;
723 Ok(Json(result))
724}
725
726pub async fn get_network(
728 State(wallet): State<WalletState>,
729) -> Result<Json<GetNetworkResult>, AppError> {
730 let result = wallet
731 .get_network("localhost")
732 .await
733 .map_err(AppError::from_wallet_error)?;
734 Ok(Json(result))
735}
736
737pub async fn get_version(
739 State(wallet): State<WalletState>,
740) -> Result<Json<GetVersionResult>, AppError> {
741 let result = wallet
742 .get_version("localhost")
743 .await
744 .map_err(AppError::from_wallet_error)?;
745 Ok(Json(result))
746}
747
748pub async fn wait_for_authentication(
750 State(wallet): State<WalletState>,
751) -> Result<Json<AuthenticatedResult>, AppError> {
752 let result = wallet
753 .wait_for_authentication("localhost")
754 .await
755 .map_err(AppError::from_wallet_error)?;
756 Ok(Json(result))
757}
758
759pub async fn get_header_for_height(
765 State(wallet): State<WalletState>,
766 headers: HeaderMap,
767 Json(args): Json<GetHeaderArgs>,
768) -> Result<Json<GetHeaderResult>, AppError> {
769 let originator = extract_originator(&headers)?;
770 let result = wallet
771 .get_header_for_height(args, &originator)
772 .await
773 .map_err(AppError::from_wallet_error)?;
774 Ok(Json(result))
775}
776
777pub async fn verify_signature(
783 State(wallet): State<WalletState>,
784 headers: HeaderMap,
785 Json(req): Json<McVerifySignatureReq>,
786) -> Result<Json<serde_json::Value>, AppError> {
787 let originator = extract_originator(&headers)?;
788 let args = VerifySignatureArgs {
789 data: req.data,
790 hash_to_directly_verify: None,
791 signature: req.signature,
792 protocol_id: parse_protocol_id(&req.protocol_id)?,
793 key_id: req.key_id,
794 counterparty: req
795 .counterparty
796 .map(|s| parse_counterparty(&s))
797 .transpose()?,
798 for_self: req.for_self,
799 };
800 let result = wallet
801 .verify_signature(args, &originator)
802 .await
803 .map_err(AppError::from_wallet_error)?;
804 Ok(Json(json!({ "valid": result.valid })))
805}
806
807pub async fn encrypt(
809 State(wallet): State<WalletState>,
810 headers: HeaderMap,
811 Json(req): Json<McEncryptReq>,
812) -> Result<Json<serde_json::Value>, AppError> {
813 let originator = extract_originator(&headers)?;
814 let args = EncryptArgs {
815 plaintext: req.plaintext,
816 protocol_id: parse_protocol_id(&req.protocol_id)?,
817 key_id: req.key_id,
818 counterparty: req
819 .counterparty
820 .map(|s| parse_counterparty(&s))
821 .transpose()?,
822 };
823 audit::record(audit::protocol_entry(
824 "encrypt",
825 level_num(args.protocol_id.security_level),
826 &args.protocol_id.protocol_name,
827 args.counterparty.as_ref().map(cp_str),
828 Some(args.key_id.clone()),
829 &originator,
830 ));
831 let result = wallet
832 .encrypt(args, &originator)
833 .await
834 .map_err(AppError::from_wallet_error)?;
835 Ok(Json(json!({ "ciphertext": result.ciphertext })))
836}
837
838pub async fn decrypt(
840 State(wallet): State<WalletState>,
841 headers: HeaderMap,
842 Json(req): Json<McDecryptReq>,
843) -> Result<Json<serde_json::Value>, AppError> {
844 let originator = extract_originator(&headers)?;
845 let args = DecryptArgs {
846 ciphertext: req.ciphertext,
847 protocol_id: parse_protocol_id(&req.protocol_id)?,
848 key_id: req.key_id,
849 counterparty: req
850 .counterparty
851 .map(|s| parse_counterparty(&s))
852 .transpose()?,
853 };
854 audit::record(audit::protocol_entry(
855 "decrypt",
856 level_num(args.protocol_id.security_level),
857 &args.protocol_id.protocol_name,
858 args.counterparty.as_ref().map(cp_str),
859 Some(args.key_id.clone()),
860 &originator,
861 ));
862 let result = wallet
863 .decrypt(args, &originator)
864 .await
865 .map_err(AppError::from_wallet_error)?;
866 Ok(Json(json!({ "plaintext": result.plaintext })))
867}
868
869pub async fn create_hmac(
871 State(wallet): State<WalletState>,
872 headers: HeaderMap,
873 Json(req): Json<McCreateHmacReq>,
874) -> Result<Json<serde_json::Value>, AppError> {
875 let originator = extract_originator(&headers)?;
876 let args = CreateHmacArgs {
877 data: req.data,
878 protocol_id: parse_protocol_id(&req.protocol_id)?,
879 key_id: req.key_id,
880 counterparty: req
881 .counterparty
882 .map(|s| parse_counterparty(&s))
883 .transpose()?,
884 };
885 audit::record(audit::protocol_entry(
886 "createHmac",
887 level_num(args.protocol_id.security_level),
888 &args.protocol_id.protocol_name,
889 args.counterparty.as_ref().map(cp_str),
890 Some(args.key_id.clone()),
891 &originator,
892 ));
893 let result = wallet
894 .create_hmac(args, &originator)
895 .await
896 .map_err(AppError::from_wallet_error)?;
897 Ok(Json(json!({ "hmac": result.hmac.to_vec() })))
898}
899
900pub async fn verify_hmac(
902 State(wallet): State<WalletState>,
903 headers: HeaderMap,
904 Json(req): Json<McVerifyHmacReq>,
905) -> Result<Json<serde_json::Value>, AppError> {
906 let originator = extract_originator(&headers)?;
907 let hmac: [u8; 32] = req
908 .hmac
909 .try_into()
910 .map_err(|_| anyhow::anyhow!("hmac must be exactly 32 bytes"))?;
911 let args = VerifyHmacArgs {
912 data: req.data,
913 hmac,
914 protocol_id: parse_protocol_id(&req.protocol_id)?,
915 key_id: req.key_id,
916 counterparty: req
917 .counterparty
918 .map(|s| parse_counterparty(&s))
919 .transpose()?,
920 };
921 let result = wallet
922 .verify_hmac(args, &originator)
923 .await
924 .map_err(AppError::from_wallet_error)?;
925 Ok(Json(json!({ "valid": result.valid })))
926}
927
928pub async fn sign_action(
934 State(wallet): State<WalletState>,
935 headers: HeaderMap,
936 Json(args): Json<SignActionArgs>,
937) -> Result<Json<McSignActionRes>, AppError> {
938 let originator = extract_originator(&headers)?;
939 let reference = args.reference.clone();
940 let result = wallet
941 .sign_action(args, &originator)
942 .await
943 .map_err(AppError::from_wallet_error)?;
944 tracing::info!(originator = %originator, reference = %reference, "signAction");
945 let broadcast = match result.txid.as_ref() {
946 Some(t) if super::broadcast_follow_up::sending(&result.send_with_results, t) => {
947 super::post_word::post_word(wallet.storage(), &to_hex(t)).await
948 }
949 _ => None,
950 };
951 Ok(Json(McSignActionRes {
955 txid: result.txid.map(|t| to_hex(&t)),
956 tx: result.tx,
957 send_with_results: result
958 .send_with_results
959 .and_then(|r| serde_json::to_value(r).ok()),
960 broadcast,
961 }))
962}
963
964pub async fn abort_action(
966 State(wallet): State<WalletState>,
967 headers: HeaderMap,
968 Json(args): Json<AbortActionArgs>,
969) -> Result<Json<AbortActionResult>, AppError> {
970 let originator = extract_originator(&headers)?;
971 if let Some(txid) =
976 crate::retire_guard::abort_refused_for(wallet.storage().pool(), &args.reference)
977 .await
978 .map_err(AppError::from_wallet_error)?
979 {
980 return Err(AppError::from_wallet_error(format!(
981 "Invalid operation: a broadcaster took transaction {txid}; it is not aborted. It stays, \
982 re-asked, until a proof, a node verdict or a competitor's checked proof decides it"
983 )));
984 }
985 let result = wallet.abort_action(args, &originator).await.map_err(|e| {
986 let mut err = AppError::from_wallet_error(e);
993 if err.message().contains("the network has transaction") {
994 err = err.with_hint(
995 "the wallet holds chain evidence for this transaction, so it \
996 cannot be aborted; if it is a phantom after all, \
997 `bsv-wallet reconcile-broadcasts --execute` re-checks it \
998 against the chain index",
999 );
1000 }
1001 err
1002 })?;
1003 Ok(Json(result))
1004}
1005
1006pub async fn list_actions(
1008 State(wallet): State<WalletState>,
1009 headers: HeaderMap,
1010 Json(args): Json<ListActionsArgs>,
1011) -> Result<Json<ListActionsResult>, AppError> {
1012 let originator = extract_originator(&headers)?;
1013 let result = wallet
1014 .list_actions(args, &originator)
1015 .await
1016 .map_err(AppError::from_wallet_error)?;
1017 Ok(Json(result))
1018}
1019
1020pub async fn list_outputs(
1022 State(wallet): State<WalletState>,
1023 headers: HeaderMap,
1024 Json(args): Json<ListOutputsArgs>,
1025) -> Result<Json<ListOutputsResult>, AppError> {
1026 let originator = extract_originator(&headers)?;
1027 let result = wallet
1028 .list_outputs(args, &originator)
1029 .await
1030 .map_err(AppError::from_wallet_error)?;
1031 Ok(Json(result))
1032}
1033
1034pub async fn relinquish_output(
1039 State(wallet): State<WalletState>,
1040 axum::Extension(spending_lock): axum::Extension<super::SpendingLock>,
1041 headers: HeaderMap,
1042 Json(args): Json<RelinquishOutputArgs>,
1043) -> Result<Json<RelinquishOutputResult>, AppError> {
1044 let originator = extract_originator(&headers)?;
1045 let _guard = spending_lock.lock().await;
1046 let result = wallet
1047 .relinquish_output(args, &originator)
1048 .await
1049 .map_err(AppError::from_wallet_error)?;
1050 Ok(Json(result))
1051}
1052
1053pub async fn acquire_certificate(
1059 State(wallet): State<WalletState>,
1060 headers: HeaderMap,
1061 Json(args): Json<AcquireCertificateArgs>,
1062) -> Result<Json<WalletCertificate>, AppError> {
1063 let originator = extract_originator(&headers)?;
1064 let result = wallet
1065 .acquire_certificate(args, &originator)
1066 .await
1067 .map_err(AppError::from_wallet_error)?;
1068 Ok(Json(result))
1069}
1070
1071pub async fn list_certificates(
1073 State(wallet): State<WalletState>,
1074 headers: HeaderMap,
1075 Json(args): Json<ListCertificatesArgs>,
1076) -> Result<Json<ListCertificatesResult>, AppError> {
1077 let originator = extract_originator(&headers)?;
1078 let result = wallet
1079 .list_certificates(args, &originator)
1080 .await
1081 .map_err(AppError::from_wallet_error)?;
1082 Ok(Json(result))
1083}
1084
1085pub async fn prove_certificate(
1087 State(wallet): State<WalletState>,
1088 headers: HeaderMap,
1089 Json(args): Json<ProveCertificateArgs>,
1090) -> Result<Json<ProveCertificateResult>, AppError> {
1091 let originator = extract_originator(&headers)?;
1092 let result = wallet
1093 .prove_certificate(args, &originator)
1094 .await
1095 .map_err(AppError::from_wallet_error)?;
1096 Ok(Json(result))
1097}
1098
1099pub async fn relinquish_certificate(
1101 State(wallet): State<WalletState>,
1102 headers: HeaderMap,
1103 Json(args): Json<RelinquishCertificateArgs>,
1104) -> Result<Json<RelinquishCertificateResult>, AppError> {
1105 let originator = extract_originator(&headers)?;
1106 let result = wallet
1107 .relinquish_certificate(args, &originator)
1108 .await
1109 .map_err(AppError::from_wallet_error)?;
1110 Ok(Json(result))
1111}
1112
1113pub async fn discover_by_identity_key(
1119 State(wallet): State<WalletState>,
1120 headers: HeaderMap,
1121 Json(args): Json<DiscoverByIdentityKeyArgs>,
1122) -> Result<Json<DiscoverCertificatesResult>, AppError> {
1123 let originator = extract_originator(&headers)?;
1124 let result = wallet
1125 .discover_by_identity_key(args, &originator)
1126 .await
1127 .map_err(AppError::from_wallet_error)?;
1128 Ok(Json(result))
1129}
1130
1131pub async fn discover_by_attributes(
1133 State(wallet): State<WalletState>,
1134 headers: HeaderMap,
1135 Json(args): Json<DiscoverByAttributesArgs>,
1136) -> Result<Json<DiscoverCertificatesResult>, AppError> {
1137 let originator = extract_originator(&headers)?;
1138 let result = wallet
1139 .discover_by_attributes(args, &originator)
1140 .await
1141 .map_err(AppError::from_wallet_error)?;
1142 Ok(Json(result))
1143}
1144
1145pub async fn reveal_counterparty_key_linkage(
1147 State(wallet): State<WalletState>,
1148 headers: HeaderMap,
1149 Json(req): Json<McRevealCounterpartyKeyLinkageReq>,
1150) -> Result<Json<RevealCounterpartyKeyLinkageResult>, AppError> {
1151 let originator = extract_originator(&headers)?;
1152 let args = WalletRevealCounterpartyArgs {
1153 counterparty: PublicKey::from_hex(&req.counterparty)
1154 .map_err(|e| anyhow::anyhow!("{}", e))?,
1155 verifier: PublicKey::from_hex(&req.verifier).map_err(|e| anyhow::anyhow!("{}", e))?,
1156 privileged: req.privileged,
1157 privileged_reason: req.privileged_reason,
1158 };
1159 let result = wallet
1160 .reveal_counterparty_key_linkage(args, &originator)
1161 .await
1162 .map_err(AppError::from_wallet_error)?;
1163 Ok(Json(result))
1164}
1165
1166pub async fn reveal_specific_key_linkage(
1168 State(wallet): State<WalletState>,
1169 headers: HeaderMap,
1170 Json(req): Json<McRevealSpecificKeyLinkageReq>,
1171) -> Result<Json<RevealSpecificKeyLinkageResult>, AppError> {
1172 let originator = extract_originator(&headers)?;
1173 let args = WalletRevealSpecificArgs {
1174 counterparty: parse_counterparty(&req.counterparty)?,
1175 verifier: PublicKey::from_hex(&req.verifier).map_err(|e| anyhow::anyhow!("{}", e))?,
1176 protocol_id: parse_protocol_id(&req.protocol_id)?,
1177 key_id: req.key_id,
1178 privileged: req.privileged,
1179 privileged_reason: req.privileged_reason,
1180 };
1181 let result = wallet
1182 .reveal_specific_key_linkage(args, &originator)
1183 .await
1184 .map_err(AppError::from_wallet_error)?;
1185 Ok(Json(result))
1186}