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 ListCertificatesArgs,
43 ListCertificatesResult,
44 ListOutputsArgs,
46 ListOutputsResult,
47 Outpoint,
48 Protocol,
49 ProveCertificateArgs,
50 ProveCertificateResult,
51 RelinquishCertificateArgs,
52 RelinquishCertificateResult,
53 RelinquishOutputArgs,
54 RelinquishOutputResult,
55 RevealCounterpartyKeyLinkageResult,
57 RevealSpecificKeyLinkageResult,
58 SecurityLevel,
59 SignActionArgs,
60 TrustSelf,
61 VerifyHmacArgs,
62 VerifySignatureArgs,
63 WalletCertificate,
64 WalletInterface,
65 WalletPayment,
66 WalletRevealCounterpartyArgs,
67 WalletRevealSpecificArgs,
68};
69use bsv_wallet_toolbox::{Services, StorageSqlx, Wallet};
70use serde_json::json;
71use std::sync::Arc;
72
73use super::types::*;
74
75pub type WalletState = Arc<Wallet<StorageSqlx, Services>>;
76
77use super::audit;
78
79fn cp_str(c: &Counterparty) -> String {
83 match c {
87 Counterparty::Self_ => "self".to_string(),
88 Counterparty::Anyone => "anyone".to_string(),
89 Counterparty::Other(pk) => pk.to_string().to_lowercase(),
90 }
91}
92
93fn level_num(l: SecurityLevel) -> u8 {
94 match l {
95 SecurityLevel::Silent => 0,
96 SecurityLevel::App => 1,
97 SecurityLevel::Counterparty => 2,
98 }
99}
100
101fn extract_originator(
103 headers: &HeaderMap,
104) -> Result<String, (StatusCode, Json<serde_json::Value>)> {
105 if let Some(origin) = headers.get("origin").and_then(|v| v.to_str().ok()) {
106 if let Ok(url) = url::Url::parse(origin) {
107 if let Some(host) = url.host_str() {
108 return Ok(host.to_string());
109 }
110 }
111 }
112 if let Some(originator) = headers.get("originator").and_then(|v| v.to_str().ok()) {
113 return Ok(originator.to_string());
114 }
115 Err((
116 StatusCode::BAD_REQUEST,
117 Json(json!({"message": "Origin header required"})),
118 ))
119}
120
121pub struct AppError {
123 status: StatusCode,
124 code: &'static str,
125 message: String,
126 refusal: Option<crate::spend_gate::Refusal>,
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("Invalid BEEF at byte") {
150 (StatusCode::BAD_REQUEST, "INVALID_BEEF")
153 } else if clean.starts_with("Validation error:")
154 || clean.starts_with("Invalid argument:")
155 || clean.starts_with("Origin header required")
156 {
157 (StatusCode::BAD_REQUEST, "VALIDATION_ERROR")
158 } else if clean.starts_with("Storage error:")
159 || clean.starts_with("Database error:")
160 || clean.starts_with("Internal error:")
161 {
162 (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR")
163 } else if clean.starts_with("Service error:")
164 || clean.starts_with("Network error:")
165 || clean.starts_with("Broadcast failed:")
166 {
167 (StatusCode::BAD_GATEWAY, "SERVICE_ERROR")
168 } else {
169 (StatusCode::BAD_REQUEST, "ERROR")
170 }
171 }
172
173 fn from_wallet_error(e: impl std::fmt::Display) -> Self {
175 let message = e.to_string();
176 let (status, code) = Self::classify(&message);
177 Self {
178 status,
179 code,
180 message,
181 refusal: None,
182 }
183 }
184
185 fn from_gate_error(e: crate::spend_gate::GateError) -> Self {
187 match e {
188 crate::spend_gate::GateError::Refused(refusal) if refusal.is_named_input() => Self {
189 status: StatusCode::FORBIDDEN,
190 code: "INPUT_NOT_OFFERED",
191 message: refusal.to_string(),
192 refusal: Some(refusal),
193 },
194 crate::spend_gate::GateError::Refused(refusal) => Self {
195 status: StatusCode::PAYMENT_REQUIRED,
196 code: "INSUFFICIENT_FUNDS",
197 message: refusal.to_string(),
198 refusal: Some(refusal),
199 },
200 other => Self::from_wallet_error(other),
201 }
202 }
203
204 fn message(&self) -> &str {
205 &self.message
206 }
207
208 fn with_hint(mut self, hint: &str) -> Self {
210 self.message = format!("{} (hint: {})", self.message, hint);
211 self
212 }
213
214 fn broadcast_rejected(txid: &str) -> Self {
221 Self {
222 status: StatusCode::BAD_GATEWAY,
223 code: "BROADCAST_REJECTED",
224 message: format!(
225 "broadcast rejected: no broadcaster accepted transaction {txid}, and it is \
226 absent from the broadcaster it was submitted to and from an independent chain \
227 index after the full probe window; this wallet retired it (failed). An input \
228 it spent is released only where the chain vouches it unspent; any other stays \
229 locked and is re-checked."
230 ),
231 refusal: None,
232 }
233 }
234}
235
236impl From<anyhow::Error> for AppError {
237 fn from(err: anyhow::Error) -> Self {
238 Self::from_wallet_error(err)
239 }
240}
241
242impl From<(StatusCode, Json<serde_json::Value>)> for AppError {
243 fn from((status, json): (StatusCode, Json<serde_json::Value>)) -> Self {
244 let message = json
245 .0
246 .get("message")
247 .and_then(|m| m.as_str())
248 .unwrap_or_default()
249 .to_string();
250 let (_, code) = Self::classify(&message);
251 Self {
252 status,
253 code,
254 message,
255 refusal: None,
256 }
257 }
258}
259
260impl IntoResponse for AppError {
261 fn into_response(self) -> Response {
262 tracing::warn!(
263 status = self.status.as_u16(),
264 code = self.code,
265 message = %self.message,
266 "request error"
267 );
268 let mut body = json!({"code": self.code, "message": self.message});
269 if let Some(refusal) = &self.refusal {
270 body["offeredSatoshis"] = json!(refusal.offered_satoshis);
271 body["held"] = refusal.held_json();
272 }
273 (self.status, Json(body)).into_response()
274 }
275}
276
277pub async fn is_authenticated() -> Json<serde_json::Value> {
279 Json(json!({"authenticated": true}))
280}
281
282pub async fn get_public_key(
284 State(wallet): State<WalletState>,
285 headers: HeaderMap,
286 Json(req): Json<McGetPublicKeyReq>,
287) -> Result<Json<McGetPublicKeyRes>, AppError> {
288 let originator = extract_originator(&headers)?;
289
290 let args = GetPublicKeyArgs {
291 identity_key: req.identity_key,
292 protocol_id: req.protocol_id.map(|v| parse_protocol_id(&v)).transpose()?,
293 key_id: req.key_id,
294 counterparty: req
295 .counterparty
296 .map(|s| parse_counterparty(&s))
297 .transpose()?,
298 for_self: req.for_self,
299 };
300
301 if let Some(p) = args.protocol_id.as_ref() {
302 audit::record(audit::protocol_entry(
303 "getPublicKey",
304 level_num(p.security_level),
305 &p.protocol_name,
306 args.counterparty.as_ref().map(cp_str),
307 args.key_id.clone(),
308 &originator,
309 ));
310 }
311 let result = wallet
312 .get_public_key(args, &originator)
313 .await
314 .map_err(AppError::from_wallet_error)?;
315
316 Ok(Json(McGetPublicKeyRes {
317 public_key: result.public_key,
318 }))
319}
320
321pub async fn create_signature(
323 State(wallet): State<WalletState>,
324 headers: HeaderMap,
325 Json(req): Json<McCreateSignatureReq>,
326) -> Result<Json<McCreateSignatureRes>, AppError> {
327 let originator = extract_originator(&headers)?;
328
329 let hash_to_directly_sign = req
332 .hash_to_directly_sign
333 .map(|h| {
334 <[u8; 32]>::try_from(h.as_slice()).map_err(|_| {
335 AppError::from_wallet_error("Validation error: hashToDirectlySign must be 32 bytes")
336 })
337 })
338 .transpose()?;
339 if req.data.is_none() && hash_to_directly_sign.is_none() {
340 return Err(AppError::from_wallet_error(
341 "Validation error: one of data or hashToDirectlySign is required",
342 ));
343 }
344
345 let args = CreateSignatureArgs {
346 data: req.data,
347 hash_to_directly_sign,
348 protocol_id: parse_protocol_id(&req.protocol_id)?,
349 key_id: req.key_id,
350 counterparty: Some(parse_counterparty(&req.counterparty)?),
351 };
352
353 audit::record(audit::protocol_entry(
354 "createSignature",
355 level_num(args.protocol_id.security_level),
356 &args.protocol_id.protocol_name,
357 args.counterparty.as_ref().map(cp_str),
358 Some(args.key_id.clone()),
359 &originator,
360 ));
361 let result = wallet
362 .create_signature(args, &originator)
363 .await
364 .map_err(AppError::from_wallet_error)?;
365
366 Ok(Json(McCreateSignatureRes {
367 signature: result.signature,
368 }))
369}
370
371pub async fn create_action(
377 State(wallet): State<WalletState>,
378 axum::Extension(spending_lock): axum::Extension<super::SpendingLock>,
379 axum::Extension(verifier): axum::Extension<crate::broadcast_verify::BroadcastVerifier>,
380 headers: HeaderMap,
381 Json(req): Json<McCreateActionReq>,
382) -> Result<Json<McCreateActionRes>, AppError> {
383 let originator = extract_originator(&headers)?;
384 let allow_received =
386 crate::spend_gate::allow_pending(req.allow_pending.as_ref().and_then(|v| v.as_str()));
387
388 let (opt_no_send, opt_accept_delayed) = req
391 .options
392 .as_ref()
393 .map(|o| {
394 (
395 o.no_send.unwrap_or(false),
396 o.accept_delayed_broadcast.unwrap_or(false),
397 )
398 })
399 .unwrap_or((false, false));
400
401 let outputs = req
402 .outputs
403 .map(|outs| {
404 outs.into_iter()
405 .map(|o| {
406 Ok(CreateActionOutput {
407 locking_script: hex::decode(&o.locking_script)?,
408 satoshis: o.satoshis,
409 output_description: o.output_description,
410 basket: o.basket,
411 custom_instructions: o.custom_instructions,
412 tags: o.tags,
413 })
414 })
415 .collect::<Result<Vec<_>>>()
416 })
417 .transpose()?;
418
419 let inputs = match req.inputs {
425 Some(reqs) if !reqs.is_empty() => {
426 let mut mapped = Vec::with_capacity(reqs.len());
427 for i in reqs {
428 let outpoint = Outpoint::from_string(&i.outpoint)
429 .map_err(|e| anyhow::anyhow!("invalid input outpoint '{}': {e}", i.outpoint))?;
430 let unlocking_script = match i.unlocking_script {
431 Some(h) => Some(hex::decode(&h).context("invalid unlockingScript hex")?),
432 None => None,
433 };
434 mapped.push(CreateActionInput {
435 outpoint,
436 input_description: i.input_description.unwrap_or_else(|| "input".to_string()),
437 unlocking_script,
438 unlocking_script_length: i.unlocking_script_length,
439 sequence_number: i.sequence_number,
440 });
441 }
442 Some(mapped)
443 }
444 _ => Some(vec![]),
445 };
446
447 let args = CreateActionArgs {
448 description: req.description,
449 input_beef: req.input_beef,
450 inputs,
451 outputs,
452 lock_time: req.lock_time,
453 version: None,
454 labels: req.labels,
455 options: req.options.map(|o| CreateActionOptions {
456 accept_delayed_broadcast: o.accept_delayed_broadcast,
457 randomize_outputs: o.randomize_outputs,
458 sign_and_process: o.sign_and_process,
459 no_send: o.no_send,
460 trust_self: match o.trust_self.as_deref() {
461 Some("known") => Some(TrustSelf::Known),
462 _ => None,
463 },
464 ..Default::default()
465 }),
466 };
467
468 let output_count = args.outputs.as_ref().map(|o| o.len()).unwrap_or(0);
469 let total_sats: u64 = args
470 .outputs
471 .as_ref()
472 .map(|o| o.iter().map(|x| x.satoshis).sum())
473 .unwrap_or(0);
474
475 audit::record(audit::spend_entry(
479 "createAction",
480 total_sats,
481 Some(args.description.clone()),
482 &originator,
483 ));
484
485 let _guard = spending_lock.lock().await;
488 crate::spend_guard::run(wallet.storage().pool())
491 .await
492 .map_err(|e| AppError::from_wallet_error(format!("Storage error: spend guard: {e}")))?;
493 let result = crate::spend_gate::create_action(
499 &wallet,
500 args,
501 &originator,
502 &crate::spend_gate::GateOptions::from_env().with_allow_received(allow_received),
503 )
504 .await
505 .map_err(AppError::from_gate_error)?;
506 drop(_guard);
507
508 tracing::info!(
509 originator = %originator,
510 outputs = output_count,
511 total_satoshis = total_sats,
512 txid = ?result.txid.as_ref().map(|t| to_hex(t)),
513 "createAction"
514 );
515
516 let atomic_beef = match (result.beef.as_ref(), result.txid.as_ref()) {
519 (Some(beef_bytes), Some(txid_bytes)) => {
520 let txid_hex = to_hex(txid_bytes);
521 match bsv_sdk::transaction::Beef::from_binary(beef_bytes) {
522 Ok(mut beef) => match beef.to_binary_atomic(&txid_hex) {
523 Ok(ab) => Some(ab),
524 Err(e) => {
525 tracing::warn!(error = %e, "Failed to build AtomicBEEF, falling back to raw tx");
526 result.tx.clone()
527 }
528 },
529 Err(e) => {
530 tracing::warn!(error = %e, "Failed to parse BEEF, falling back to raw tx");
531 result.tx.clone()
532 }
533 }
534 }
535 _ => result.tx.clone(),
536 };
537
538 let disposition =
552 super::broadcast_follow_up::disposition(&result, opt_no_send, opt_accept_delayed);
553 if let Some(txid_bytes) = result.txid.as_ref() {
554 let txid_hex = to_hex(txid_bytes);
555 let probe = verifier.clone();
556 let report_wallet = wallet.clone();
557 let ancestors: Vec<String> = result
560 .beef
561 .as_deref()
562 .map(|beef| bsv_wallet_toolbox::unproven_ancestors_in_beef(beef, &txid_hex))
563 .unwrap_or_default();
564 let outcome = super::broadcast_follow_up::follow_up(
565 disposition,
566 txid_hex.clone(),
567 move |txid| async move { probe.verify_report(&txid).await },
568 move |txid, report| async move {
569 super::broadcast_follow_up::apply_presence_report(
570 report_wallet.storage(),
571 report_wallet.services(),
572 &txid,
573 &ancestors,
574 &report,
575 )
576 .await;
577 },
578 )
579 .await;
580 if outcome == super::broadcast_follow_up::FollowUp::Rejected
581 && super::broadcast_follow_up::tx_status(wallet.storage(), &txid_hex).await
582 == Some("failed".to_string())
583 {
584 return Err(AppError::broadcast_rejected(&txid_hex));
585 }
586 }
587 let broadcast = match result.txid.as_ref() {
588 Some(t) if super::broadcast_follow_up::sending(&result.send_with_results, t) => {
589 super::post_word::post_word(wallet.storage(), &to_hex(t)).await
590 }
591 _ => None,
592 };
593
594 Ok(Json(McCreateActionRes {
595 txid: result.txid.map(|t| to_hex(&t)),
596 tx: atomic_beef,
597 send_with_results: result
598 .send_with_results
599 .and_then(|r| serde_json::to_value(r).ok()),
600 signable_transaction: result.signable_transaction.map(|st| {
601 let reference = crate::spend_gate::reference_string(&st.reference);
605 McSignableTransaction {
606 tx: st.tx,
607 reference,
608 }
609 }),
610 no_send_change: result
611 .no_send_change
612 .and_then(|v| serde_json::to_value(v).ok()),
613 broadcast,
614 }))
615}
616
617pub async fn internalize_action(
619 State(wallet): State<WalletState>,
620 headers: HeaderMap,
621 Json(req): Json<McInternalizeActionReq>,
622) -> Result<Json<McInternalizeActionRes>, AppError> {
623 let originator = extract_originator(&headers)?;
624
625 let args = InternalizeActionArgs {
626 tx: req.tx,
627 outputs: req
628 .outputs
629 .into_iter()
630 .map(|o| InternalizeOutput {
631 output_index: o.output_index,
632 protocol: o.protocol,
633 payment_remittance: o.payment_remittance.map(|p| WalletPayment {
634 derivation_prefix: p.derivation_prefix,
635 derivation_suffix: p.derivation_suffix,
636 sender_identity_key: p.sender_identity_key,
637 }),
638 insertion_remittance: o.insertion_remittance.map(|i| BasketInsertion {
639 basket: i.basket,
640 custom_instructions: i.custom_instructions,
641 tags: i.tags,
642 }),
643 })
644 .collect(),
645 description: req.description,
646 labels: req.labels,
647 seek_permission: None,
648 };
649
650 let output_count = args.outputs.len();
651 let subject = super::post_word::atomic_subject(&args.tx);
652 let result = wallet
653 .internalize_action(args, &originator)
654 .await
655 .map_err(AppError::from_wallet_error)?;
656
657 tracing::info!(
658 originator = %originator,
659 outputs = output_count,
660 accepted = result.accepted,
661 "internalizeAction"
662 );
663 let broadcast = match subject {
666 Some(txid) => super::post_word::post_word(wallet.storage(), &txid).await,
667 None => None,
668 };
669
670 Ok(Json(McInternalizeActionRes {
671 accepted: result.accepted,
672 broadcast,
673 }))
674}
675
676fn parse_protocol_id(v: &serde_json::Value) -> Result<Protocol> {
680 let arr = v.as_array().context("protocolID must be an array")?;
681 let level = arr
682 .first()
683 .and_then(|v| v.as_u64())
684 .context("protocolID[0] must be an integer")?;
685 let name = arr
686 .get(1)
687 .and_then(|v| v.as_str())
688 .context("protocolID[1] must be a string")?;
689 Ok(Protocol::new(
690 match level {
691 0 => SecurityLevel::Silent,
692 1 => SecurityLevel::App,
693 2 => SecurityLevel::Counterparty,
694 _ => anyhow::bail!("invalid security level: {level}"),
695 },
696 name,
697 ))
698}
699
700fn parse_counterparty(s: &str) -> Result<Counterparty> {
702 match s {
703 "self" => Ok(Counterparty::Self_),
704 "anyone" => Ok(Counterparty::Anyone),
705 hex => Ok(Counterparty::Other(PublicKey::from_hex(hex)?)),
706 }
707}
708
709pub async fn get_height(
715 State(wallet): State<WalletState>,
716) -> Result<Json<GetHeightResult>, AppError> {
717 let result = wallet
718 .get_height("localhost")
719 .await
720 .map_err(AppError::from_wallet_error)?;
721 Ok(Json(result))
722}
723
724pub async fn get_network(
726 State(wallet): State<WalletState>,
727) -> Result<Json<GetNetworkResult>, AppError> {
728 let result = wallet
729 .get_network("localhost")
730 .await
731 .map_err(AppError::from_wallet_error)?;
732 Ok(Json(result))
733}
734
735pub async fn get_version(
737 State(wallet): State<WalletState>,
738) -> Result<Json<GetVersionResult>, AppError> {
739 let result = wallet
740 .get_version("localhost")
741 .await
742 .map_err(AppError::from_wallet_error)?;
743 Ok(Json(result))
744}
745
746pub async fn wait_for_authentication(
748 State(wallet): State<WalletState>,
749) -> Result<Json<AuthenticatedResult>, AppError> {
750 let result = wallet
751 .wait_for_authentication("localhost")
752 .await
753 .map_err(AppError::from_wallet_error)?;
754 Ok(Json(result))
755}
756
757pub async fn get_header_for_height(
763 State(wallet): State<WalletState>,
764 headers: HeaderMap,
765 Json(args): Json<GetHeaderArgs>,
766) -> Result<Json<GetHeaderResult>, AppError> {
767 let originator = extract_originator(&headers)?;
768 let result = wallet
769 .get_header_for_height(args, &originator)
770 .await
771 .map_err(AppError::from_wallet_error)?;
772 Ok(Json(result))
773}
774
775pub async fn verify_signature(
781 State(wallet): State<WalletState>,
782 headers: HeaderMap,
783 Json(req): Json<McVerifySignatureReq>,
784) -> Result<Json<serde_json::Value>, AppError> {
785 let originator = extract_originator(&headers)?;
786 let args = VerifySignatureArgs {
787 data: req.data,
788 hash_to_directly_verify: None,
789 signature: req.signature,
790 protocol_id: parse_protocol_id(&req.protocol_id)?,
791 key_id: req.key_id,
792 counterparty: req
793 .counterparty
794 .map(|s| parse_counterparty(&s))
795 .transpose()?,
796 for_self: req.for_self,
797 };
798 let result = wallet
799 .verify_signature(args, &originator)
800 .await
801 .map_err(AppError::from_wallet_error)?;
802 Ok(Json(json!({ "valid": result.valid })))
803}
804
805pub async fn encrypt(
807 State(wallet): State<WalletState>,
808 headers: HeaderMap,
809 Json(req): Json<McEncryptReq>,
810) -> Result<Json<serde_json::Value>, AppError> {
811 let originator = extract_originator(&headers)?;
812 let args = EncryptArgs {
813 plaintext: req.plaintext,
814 protocol_id: parse_protocol_id(&req.protocol_id)?,
815 key_id: req.key_id,
816 counterparty: req
817 .counterparty
818 .map(|s| parse_counterparty(&s))
819 .transpose()?,
820 };
821 audit::record(audit::protocol_entry(
822 "encrypt",
823 level_num(args.protocol_id.security_level),
824 &args.protocol_id.protocol_name,
825 args.counterparty.as_ref().map(cp_str),
826 Some(args.key_id.clone()),
827 &originator,
828 ));
829 let result = wallet
830 .encrypt(args, &originator)
831 .await
832 .map_err(AppError::from_wallet_error)?;
833 Ok(Json(json!({ "ciphertext": result.ciphertext })))
834}
835
836pub async fn decrypt(
838 State(wallet): State<WalletState>,
839 headers: HeaderMap,
840 Json(req): Json<McDecryptReq>,
841) -> Result<Json<serde_json::Value>, AppError> {
842 let originator = extract_originator(&headers)?;
843 let args = DecryptArgs {
844 ciphertext: req.ciphertext,
845 protocol_id: parse_protocol_id(&req.protocol_id)?,
846 key_id: req.key_id,
847 counterparty: req
848 .counterparty
849 .map(|s| parse_counterparty(&s))
850 .transpose()?,
851 };
852 audit::record(audit::protocol_entry(
853 "decrypt",
854 level_num(args.protocol_id.security_level),
855 &args.protocol_id.protocol_name,
856 args.counterparty.as_ref().map(cp_str),
857 Some(args.key_id.clone()),
858 &originator,
859 ));
860 let result = wallet
861 .decrypt(args, &originator)
862 .await
863 .map_err(AppError::from_wallet_error)?;
864 Ok(Json(json!({ "plaintext": result.plaintext })))
865}
866
867pub async fn create_hmac(
869 State(wallet): State<WalletState>,
870 headers: HeaderMap,
871 Json(req): Json<McCreateHmacReq>,
872) -> Result<Json<serde_json::Value>, AppError> {
873 let originator = extract_originator(&headers)?;
874 let args = CreateHmacArgs {
875 data: req.data,
876 protocol_id: parse_protocol_id(&req.protocol_id)?,
877 key_id: req.key_id,
878 counterparty: req
879 .counterparty
880 .map(|s| parse_counterparty(&s))
881 .transpose()?,
882 };
883 audit::record(audit::protocol_entry(
884 "createHmac",
885 level_num(args.protocol_id.security_level),
886 &args.protocol_id.protocol_name,
887 args.counterparty.as_ref().map(cp_str),
888 Some(args.key_id.clone()),
889 &originator,
890 ));
891 let result = wallet
892 .create_hmac(args, &originator)
893 .await
894 .map_err(AppError::from_wallet_error)?;
895 Ok(Json(json!({ "hmac": result.hmac.to_vec() })))
896}
897
898pub async fn verify_hmac(
900 State(wallet): State<WalletState>,
901 headers: HeaderMap,
902 Json(req): Json<McVerifyHmacReq>,
903) -> Result<Json<serde_json::Value>, AppError> {
904 let originator = extract_originator(&headers)?;
905 let hmac: [u8; 32] = req
906 .hmac
907 .try_into()
908 .map_err(|_| anyhow::anyhow!("hmac must be exactly 32 bytes"))?;
909 let args = VerifyHmacArgs {
910 data: req.data,
911 hmac,
912 protocol_id: parse_protocol_id(&req.protocol_id)?,
913 key_id: req.key_id,
914 counterparty: req
915 .counterparty
916 .map(|s| parse_counterparty(&s))
917 .transpose()?,
918 };
919 let result = wallet
920 .verify_hmac(args, &originator)
921 .await
922 .map_err(AppError::from_wallet_error)?;
923 Ok(Json(json!({ "valid": result.valid })))
924}
925
926pub async fn sign_action(
932 State(wallet): State<WalletState>,
933 headers: HeaderMap,
934 Json(args): Json<SignActionArgs>,
935) -> Result<Json<McSignActionRes>, AppError> {
936 let originator = extract_originator(&headers)?;
937 let reference = args.reference.clone();
938 let result = wallet
939 .sign_action(args, &originator)
940 .await
941 .map_err(AppError::from_wallet_error)?;
942 tracing::info!(originator = %originator, reference = %reference, "signAction");
943 if let Some(t) = result.txid.as_ref() {
947 if let Err(e) =
948 crate::spend_gate::fill_txid(wallet.storage().pool(), &reference, &to_hex(t)).await
949 {
950 tracing::error!(reference = %reference, error = %e, "signAction: the rule's txid not recorded");
951 }
952 }
953 let broadcast = match result.txid.as_ref() {
954 Some(t) if super::broadcast_follow_up::sending(&result.send_with_results, t) => {
955 super::post_word::post_word(wallet.storage(), &to_hex(t)).await
956 }
957 _ => None,
958 };
959 Ok(Json(McSignActionRes {
963 txid: result.txid.map(|t| to_hex(&t)),
964 tx: result.tx,
965 send_with_results: result
966 .send_with_results
967 .and_then(|r| serde_json::to_value(r).ok()),
968 broadcast,
969 }))
970}
971
972pub async fn abort_action(
974 State(wallet): State<WalletState>,
975 headers: HeaderMap,
976 Json(args): Json<AbortActionArgs>,
977) -> Result<Json<AbortActionResult>, AppError> {
978 let originator = extract_originator(&headers)?;
979 if let Some(txid) =
984 crate::retire_guard::abort_refused_for(wallet.storage().pool(), &args.reference)
985 .await
986 .map_err(AppError::from_wallet_error)?
987 {
988 return Err(AppError::from_wallet_error(format!(
989 "Invalid operation: a broadcaster took transaction {txid}; it is not aborted. It stays, \
990 re-asked, until a proof, a node verdict or a competitor's checked proof decides it"
991 )));
992 }
993 let result = wallet.abort_action(args, &originator).await.map_err(|e| {
994 let mut err = AppError::from_wallet_error(e);
1001 if err.message().contains("the network has transaction") {
1002 err = err.with_hint(
1003 "the wallet holds chain evidence for this transaction, so it \
1004 cannot be aborted; if it is a phantom after all, \
1005 `bsv-wallet reconcile-broadcasts --execute` re-checks it \
1006 against the chain index",
1007 );
1008 }
1009 err
1010 })?;
1011 Ok(Json(result))
1012}
1013
1014pub async fn list_actions(
1016 State(wallet): State<WalletState>,
1017 headers: HeaderMap,
1018 Json(args): Json<ListActionsArgs>,
1019) -> Result<Json<serde_json::Value>, AppError> {
1020 let originator = extract_originator(&headers)?;
1021 let result = wallet
1022 .list_actions(args, &originator)
1023 .await
1024 .map_err(AppError::from_wallet_error)?;
1025 let rules = crate::spend_gate::action_rules(wallet.storage().pool())
1026 .await
1027 .map_err(|e| AppError::from_wallet_error(format!("Storage error: {e}")))?;
1028 let result = serde_json::to_value(result)
1029 .map_err(|e| AppError::from_wallet_error(format!("Internal error: {e}")))?;
1030 Ok(Json(with_rules(result, &rules)))
1031}
1032
1033pub fn with_rules(
1037 mut actions: serde_json::Value,
1038 rules: &crate::spend_gate::ActionRules,
1039) -> serde_json::Value {
1040 if let Some(list) = actions.get_mut("actions").and_then(|a| a.as_array_mut()) {
1041 for action in list {
1042 let txid = action
1043 .get("txid")
1044 .and_then(|t| t.as_str())
1045 .map(str::to_ascii_lowercase);
1046 let rule = match txid {
1047 Some(t) if t.bytes().all(|b| b == b'0') => {
1048 let key = crate::spend_gate::template_key(
1049 action.get("status").and_then(|v| v.as_str()).unwrap_or(""),
1050 action
1051 .get("description")
1052 .and_then(|v| v.as_str())
1053 .unwrap_or(""),
1054 action.get("satoshis").and_then(|v| v.as_i64()).unwrap_or(0),
1055 );
1056 rules.by_template.get(&key)
1057 }
1058 Some(t) => rules.by_txid.get(&t),
1059 None => None,
1060 }
1061 .cloned();
1062 if let (Some(rule), Some(obj)) = (rule, action.as_object_mut()) {
1063 obj.insert("underRule".into(), json!(rule));
1064 }
1065 }
1066 }
1067 actions
1068}
1069
1070pub async fn list_outputs(
1072 State(wallet): State<WalletState>,
1073 headers: HeaderMap,
1074 Json(args): Json<ListOutputsArgs>,
1075) -> Result<Json<ListOutputsResult>, AppError> {
1076 let originator = extract_originator(&headers)?;
1077 let result = wallet
1078 .list_outputs(args, &originator)
1079 .await
1080 .map_err(AppError::from_wallet_error)?;
1081 Ok(Json(result))
1082}
1083
1084pub async fn relinquish_output(
1089 State(wallet): State<WalletState>,
1090 axum::Extension(spending_lock): axum::Extension<super::SpendingLock>,
1091 headers: HeaderMap,
1092 Json(args): Json<RelinquishOutputArgs>,
1093) -> Result<Json<RelinquishOutputResult>, AppError> {
1094 let originator = extract_originator(&headers)?;
1095 let _guard = spending_lock.lock().await;
1096 let result = wallet
1097 .relinquish_output(args, &originator)
1098 .await
1099 .map_err(AppError::from_wallet_error)?;
1100 Ok(Json(result))
1101}
1102
1103pub async fn acquire_certificate(
1109 State(wallet): State<WalletState>,
1110 headers: HeaderMap,
1111 Json(args): Json<AcquireCertificateArgs>,
1112) -> Result<Json<WalletCertificate>, AppError> {
1113 let originator = extract_originator(&headers)?;
1114 let result = wallet
1115 .acquire_certificate(args, &originator)
1116 .await
1117 .map_err(AppError::from_wallet_error)?;
1118 Ok(Json(result))
1119}
1120
1121pub async fn list_certificates(
1123 State(wallet): State<WalletState>,
1124 headers: HeaderMap,
1125 Json(args): Json<ListCertificatesArgs>,
1126) -> Result<Json<ListCertificatesResult>, AppError> {
1127 let originator = extract_originator(&headers)?;
1128 let result = wallet
1129 .list_certificates(args, &originator)
1130 .await
1131 .map_err(AppError::from_wallet_error)?;
1132 Ok(Json(result))
1133}
1134
1135pub async fn prove_certificate(
1137 State(wallet): State<WalletState>,
1138 headers: HeaderMap,
1139 Json(args): Json<ProveCertificateArgs>,
1140) -> Result<Json<ProveCertificateResult>, AppError> {
1141 let originator = extract_originator(&headers)?;
1142 let result = wallet
1143 .prove_certificate(args, &originator)
1144 .await
1145 .map_err(AppError::from_wallet_error)?;
1146 Ok(Json(result))
1147}
1148
1149pub async fn relinquish_certificate(
1151 State(wallet): State<WalletState>,
1152 headers: HeaderMap,
1153 Json(args): Json<RelinquishCertificateArgs>,
1154) -> Result<Json<RelinquishCertificateResult>, AppError> {
1155 let originator = extract_originator(&headers)?;
1156 let result = wallet
1157 .relinquish_certificate(args, &originator)
1158 .await
1159 .map_err(AppError::from_wallet_error)?;
1160 Ok(Json(result))
1161}
1162
1163pub async fn discover_by_identity_key(
1169 State(wallet): State<WalletState>,
1170 headers: HeaderMap,
1171 Json(args): Json<DiscoverByIdentityKeyArgs>,
1172) -> Result<Json<DiscoverCertificatesResult>, AppError> {
1173 let originator = extract_originator(&headers)?;
1174 let result = wallet
1175 .discover_by_identity_key(args, &originator)
1176 .await
1177 .map_err(AppError::from_wallet_error)?;
1178 Ok(Json(result))
1179}
1180
1181pub async fn discover_by_attributes(
1183 State(wallet): State<WalletState>,
1184 headers: HeaderMap,
1185 Json(args): Json<DiscoverByAttributesArgs>,
1186) -> Result<Json<DiscoverCertificatesResult>, AppError> {
1187 let originator = extract_originator(&headers)?;
1188 let result = wallet
1189 .discover_by_attributes(args, &originator)
1190 .await
1191 .map_err(AppError::from_wallet_error)?;
1192 Ok(Json(result))
1193}
1194
1195pub async fn reveal_counterparty_key_linkage(
1197 State(wallet): State<WalletState>,
1198 headers: HeaderMap,
1199 Json(req): Json<McRevealCounterpartyKeyLinkageReq>,
1200) -> Result<Json<RevealCounterpartyKeyLinkageResult>, AppError> {
1201 let originator = extract_originator(&headers)?;
1202 let args = WalletRevealCounterpartyArgs {
1203 counterparty: PublicKey::from_hex(&req.counterparty)
1204 .map_err(|e| anyhow::anyhow!("{}", e))?,
1205 verifier: PublicKey::from_hex(&req.verifier).map_err(|e| anyhow::anyhow!("{}", e))?,
1206 privileged: req.privileged,
1207 privileged_reason: req.privileged_reason,
1208 };
1209 let result = wallet
1210 .reveal_counterparty_key_linkage(args, &originator)
1211 .await
1212 .map_err(AppError::from_wallet_error)?;
1213 Ok(Json(result))
1214}
1215
1216pub async fn reveal_specific_key_linkage(
1218 State(wallet): State<WalletState>,
1219 headers: HeaderMap,
1220 Json(req): Json<McRevealSpecificKeyLinkageReq>,
1221) -> Result<Json<RevealSpecificKeyLinkageResult>, AppError> {
1222 let originator = extract_originator(&headers)?;
1223 let args = WalletRevealSpecificArgs {
1224 counterparty: parse_counterparty(&req.counterparty)?,
1225 verifier: PublicKey::from_hex(&req.verifier).map_err(|e| anyhow::anyhow!("{}", e))?,
1226 protocol_id: parse_protocol_id(&req.protocol_id)?,
1227 key_id: req.key_id,
1228 privileged: req.privileged,
1229 privileged_reason: req.privileged_reason,
1230 };
1231 let result = wallet
1232 .reveal_specific_key_linkage(args, &originator)
1233 .await
1234 .map_err(AppError::from_wallet_error)?;
1235 Ok(Json(result))
1236}