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"] = json!(refusal
272 .held
273 .iter()
274 .map(|c| json!({
275 "outpoint": c.outpoint,
276 "satoshis": c.satoshis,
277 "word": c.word,
278 "origin": c.origin,
279 "ageMinutes": c.age_minutes,
280 "reason": c.reason,
281 }))
282 .collect::<Vec<_>>());
283 }
284 (self.status, Json(body)).into_response()
285 }
286}
287
288pub async fn is_authenticated() -> Json<serde_json::Value> {
290 Json(json!({"authenticated": true}))
291}
292
293pub async fn get_public_key(
295 State(wallet): State<WalletState>,
296 headers: HeaderMap,
297 Json(req): Json<McGetPublicKeyReq>,
298) -> Result<Json<McGetPublicKeyRes>, AppError> {
299 let originator = extract_originator(&headers)?;
300
301 let args = GetPublicKeyArgs {
302 identity_key: req.identity_key,
303 protocol_id: req.protocol_id.map(|v| parse_protocol_id(&v)).transpose()?,
304 key_id: req.key_id,
305 counterparty: req
306 .counterparty
307 .map(|s| parse_counterparty(&s))
308 .transpose()?,
309 for_self: req.for_self,
310 };
311
312 if let Some(p) = args.protocol_id.as_ref() {
313 audit::record(audit::protocol_entry(
314 "getPublicKey",
315 level_num(p.security_level),
316 &p.protocol_name,
317 args.counterparty.as_ref().map(cp_str),
318 args.key_id.clone(),
319 &originator,
320 ));
321 }
322 let result = wallet
323 .get_public_key(args, &originator)
324 .await
325 .map_err(AppError::from_wallet_error)?;
326
327 Ok(Json(McGetPublicKeyRes {
328 public_key: result.public_key,
329 }))
330}
331
332pub async fn create_signature(
334 State(wallet): State<WalletState>,
335 headers: HeaderMap,
336 Json(req): Json<McCreateSignatureReq>,
337) -> Result<Json<McCreateSignatureRes>, AppError> {
338 let originator = extract_originator(&headers)?;
339
340 let hash_to_directly_sign = req
343 .hash_to_directly_sign
344 .map(|h| {
345 <[u8; 32]>::try_from(h.as_slice()).map_err(|_| {
346 AppError::from_wallet_error("Validation error: hashToDirectlySign must be 32 bytes")
347 })
348 })
349 .transpose()?;
350 if req.data.is_none() && hash_to_directly_sign.is_none() {
351 return Err(AppError::from_wallet_error(
352 "Validation error: one of data or hashToDirectlySign is required",
353 ));
354 }
355
356 let args = CreateSignatureArgs {
357 data: req.data,
358 hash_to_directly_sign,
359 protocol_id: parse_protocol_id(&req.protocol_id)?,
360 key_id: req.key_id,
361 counterparty: Some(parse_counterparty(&req.counterparty)?),
362 };
363
364 audit::record(audit::protocol_entry(
365 "createSignature",
366 level_num(args.protocol_id.security_level),
367 &args.protocol_id.protocol_name,
368 args.counterparty.as_ref().map(cp_str),
369 Some(args.key_id.clone()),
370 &originator,
371 ));
372 let result = wallet
373 .create_signature(args, &originator)
374 .await
375 .map_err(AppError::from_wallet_error)?;
376
377 Ok(Json(McCreateSignatureRes {
378 signature: result.signature,
379 }))
380}
381
382pub async fn create_action(
388 State(wallet): State<WalletState>,
389 axum::Extension(spending_lock): axum::Extension<super::SpendingLock>,
390 axum::Extension(verifier): axum::Extension<crate::broadcast_verify::BroadcastVerifier>,
391 headers: HeaderMap,
392 Json(req): Json<McCreateActionReq>,
393) -> Result<Json<McCreateActionRes>, AppError> {
394 let originator = extract_originator(&headers)?;
395 let allow_received =
397 crate::spend_gate::allow_pending(req.allow_pending.as_ref().and_then(|v| v.as_str()));
398
399 let (opt_no_send, opt_accept_delayed) = req
402 .options
403 .as_ref()
404 .map(|o| {
405 (
406 o.no_send.unwrap_or(false),
407 o.accept_delayed_broadcast.unwrap_or(false),
408 )
409 })
410 .unwrap_or((false, false));
411
412 let outputs = req
413 .outputs
414 .map(|outs| {
415 outs.into_iter()
416 .map(|o| {
417 Ok(CreateActionOutput {
418 locking_script: hex::decode(&o.locking_script)?,
419 satoshis: o.satoshis,
420 output_description: o.output_description,
421 basket: o.basket,
422 custom_instructions: o.custom_instructions,
423 tags: o.tags,
424 })
425 })
426 .collect::<Result<Vec<_>>>()
427 })
428 .transpose()?;
429
430 let inputs = match req.inputs {
436 Some(reqs) if !reqs.is_empty() => {
437 let mut mapped = Vec::with_capacity(reqs.len());
438 for i in reqs {
439 let outpoint = Outpoint::from_string(&i.outpoint)
440 .map_err(|e| anyhow::anyhow!("invalid input outpoint '{}': {e}", i.outpoint))?;
441 let unlocking_script = match i.unlocking_script {
442 Some(h) => Some(hex::decode(&h).context("invalid unlockingScript hex")?),
443 None => None,
444 };
445 mapped.push(CreateActionInput {
446 outpoint,
447 input_description: i.input_description.unwrap_or_else(|| "input".to_string()),
448 unlocking_script,
449 unlocking_script_length: i.unlocking_script_length,
450 sequence_number: i.sequence_number,
451 });
452 }
453 Some(mapped)
454 }
455 _ => Some(vec![]),
456 };
457
458 let args = CreateActionArgs {
459 description: req.description,
460 input_beef: req.input_beef,
461 inputs,
462 outputs,
463 lock_time: req.lock_time,
464 version: None,
465 labels: req.labels,
466 options: req.options.map(|o| CreateActionOptions {
467 accept_delayed_broadcast: o.accept_delayed_broadcast,
468 randomize_outputs: o.randomize_outputs,
469 sign_and_process: o.sign_and_process,
470 no_send: o.no_send,
471 trust_self: match o.trust_self.as_deref() {
472 Some("known") => Some(TrustSelf::Known),
473 _ => None,
474 },
475 ..Default::default()
476 }),
477 };
478
479 let output_count = args.outputs.as_ref().map(|o| o.len()).unwrap_or(0);
480 let total_sats: u64 = args
481 .outputs
482 .as_ref()
483 .map(|o| o.iter().map(|x| x.satoshis).sum())
484 .unwrap_or(0);
485
486 audit::record(audit::spend_entry(
490 "createAction",
491 total_sats,
492 Some(args.description.clone()),
493 &originator,
494 ));
495
496 let _guard = spending_lock.lock().await;
499 crate::spend_guard::run(wallet.storage().pool())
502 .await
503 .map_err(|e| AppError::from_wallet_error(format!("Storage error: spend guard: {e}")))?;
504 let result = crate::spend_gate::create_action(
510 &wallet,
511 args,
512 &originator,
513 &crate::spend_gate::GateOptions::from_env().with_allow_received(allow_received),
514 )
515 .await
516 .map_err(AppError::from_gate_error)?;
517 drop(_guard);
518
519 tracing::info!(
520 originator = %originator,
521 outputs = output_count,
522 total_satoshis = total_sats,
523 txid = ?result.txid.as_ref().map(|t| to_hex(t)),
524 "createAction"
525 );
526
527 let atomic_beef = match (result.beef.as_ref(), result.txid.as_ref()) {
530 (Some(beef_bytes), Some(txid_bytes)) => {
531 let txid_hex = to_hex(txid_bytes);
532 match bsv_sdk::transaction::Beef::from_binary(beef_bytes) {
533 Ok(mut beef) => match beef.to_binary_atomic(&txid_hex) {
534 Ok(ab) => Some(ab),
535 Err(e) => {
536 tracing::warn!(error = %e, "Failed to build AtomicBEEF, falling back to raw tx");
537 result.tx.clone()
538 }
539 },
540 Err(e) => {
541 tracing::warn!(error = %e, "Failed to parse BEEF, falling back to raw tx");
542 result.tx.clone()
543 }
544 }
545 }
546 _ => result.tx.clone(),
547 };
548
549 let disposition =
563 super::broadcast_follow_up::disposition(&result, opt_no_send, opt_accept_delayed);
564 if let Some(txid_bytes) = result.txid.as_ref() {
565 let txid_hex = to_hex(txid_bytes);
566 let probe = verifier.clone();
567 let report_wallet = wallet.clone();
568 let ancestors: Vec<String> = result
571 .beef
572 .as_deref()
573 .map(|beef| bsv_wallet_toolbox::unproven_ancestors_in_beef(beef, &txid_hex))
574 .unwrap_or_default();
575 let outcome = super::broadcast_follow_up::follow_up(
576 disposition,
577 txid_hex.clone(),
578 move |txid| async move { probe.verify_report(&txid).await },
579 move |txid, report| async move {
580 super::broadcast_follow_up::apply_presence_report(
581 report_wallet.storage(),
582 report_wallet.services(),
583 &txid,
584 &ancestors,
585 &report,
586 )
587 .await;
588 },
589 )
590 .await;
591 if outcome == super::broadcast_follow_up::FollowUp::Rejected
592 && super::broadcast_follow_up::tx_status(wallet.storage(), &txid_hex).await
593 == Some("failed".to_string())
594 {
595 return Err(AppError::broadcast_rejected(&txid_hex));
596 }
597 }
598 let broadcast = match result.txid.as_ref() {
599 Some(t) if super::broadcast_follow_up::sending(&result.send_with_results, t) => {
600 super::post_word::post_word(wallet.storage(), &to_hex(t)).await
601 }
602 _ => None,
603 };
604
605 Ok(Json(McCreateActionRes {
606 txid: result.txid.map(|t| to_hex(&t)),
607 tx: atomic_beef,
608 send_with_results: result
609 .send_with_results
610 .and_then(|r| serde_json::to_value(r).ok()),
611 signable_transaction: result.signable_transaction.map(|st| {
612 let reference = crate::spend_gate::reference_string(&st.reference);
616 McSignableTransaction {
617 tx: st.tx,
618 reference,
619 }
620 }),
621 no_send_change: result
622 .no_send_change
623 .and_then(|v| serde_json::to_value(v).ok()),
624 broadcast,
625 }))
626}
627
628pub async fn internalize_action(
630 State(wallet): State<WalletState>,
631 headers: HeaderMap,
632 Json(req): Json<McInternalizeActionReq>,
633) -> Result<Json<McInternalizeActionRes>, AppError> {
634 let originator = extract_originator(&headers)?;
635
636 let args = InternalizeActionArgs {
637 tx: req.tx,
638 outputs: req
639 .outputs
640 .into_iter()
641 .map(|o| InternalizeOutput {
642 output_index: o.output_index,
643 protocol: o.protocol,
644 payment_remittance: o.payment_remittance.map(|p| WalletPayment {
645 derivation_prefix: p.derivation_prefix,
646 derivation_suffix: p.derivation_suffix,
647 sender_identity_key: p.sender_identity_key,
648 }),
649 insertion_remittance: o.insertion_remittance.map(|i| BasketInsertion {
650 basket: i.basket,
651 custom_instructions: i.custom_instructions,
652 tags: i.tags,
653 }),
654 })
655 .collect(),
656 description: req.description,
657 labels: req.labels,
658 seek_permission: None,
659 };
660
661 let output_count = args.outputs.len();
662 let subject = super::post_word::atomic_subject(&args.tx);
663 let result = wallet
664 .internalize_action(args, &originator)
665 .await
666 .map_err(AppError::from_wallet_error)?;
667
668 tracing::info!(
669 originator = %originator,
670 outputs = output_count,
671 accepted = result.accepted,
672 "internalizeAction"
673 );
674 let broadcast = match subject {
677 Some(txid) => super::post_word::post_word(wallet.storage(), &txid).await,
678 None => None,
679 };
680
681 Ok(Json(McInternalizeActionRes {
682 accepted: result.accepted,
683 broadcast,
684 }))
685}
686
687fn parse_protocol_id(v: &serde_json::Value) -> Result<Protocol> {
691 let arr = v.as_array().context("protocolID must be an array")?;
692 let level = arr
693 .first()
694 .and_then(|v| v.as_u64())
695 .context("protocolID[0] must be an integer")?;
696 let name = arr
697 .get(1)
698 .and_then(|v| v.as_str())
699 .context("protocolID[1] must be a string")?;
700 Ok(Protocol::new(
701 match level {
702 0 => SecurityLevel::Silent,
703 1 => SecurityLevel::App,
704 2 => SecurityLevel::Counterparty,
705 _ => anyhow::bail!("invalid security level: {level}"),
706 },
707 name,
708 ))
709}
710
711fn parse_counterparty(s: &str) -> Result<Counterparty> {
713 match s {
714 "self" => Ok(Counterparty::Self_),
715 "anyone" => Ok(Counterparty::Anyone),
716 hex => Ok(Counterparty::Other(PublicKey::from_hex(hex)?)),
717 }
718}
719
720pub async fn get_height(
726 State(wallet): State<WalletState>,
727) -> Result<Json<GetHeightResult>, AppError> {
728 let result = wallet
729 .get_height("localhost")
730 .await
731 .map_err(AppError::from_wallet_error)?;
732 Ok(Json(result))
733}
734
735pub async fn get_network(
737 State(wallet): State<WalletState>,
738) -> Result<Json<GetNetworkResult>, AppError> {
739 let result = wallet
740 .get_network("localhost")
741 .await
742 .map_err(AppError::from_wallet_error)?;
743 Ok(Json(result))
744}
745
746pub async fn get_version(
748 State(wallet): State<WalletState>,
749) -> Result<Json<GetVersionResult>, AppError> {
750 let result = wallet
751 .get_version("localhost")
752 .await
753 .map_err(AppError::from_wallet_error)?;
754 Ok(Json(result))
755}
756
757pub async fn wait_for_authentication(
759 State(wallet): State<WalletState>,
760) -> Result<Json<AuthenticatedResult>, AppError> {
761 let result = wallet
762 .wait_for_authentication("localhost")
763 .await
764 .map_err(AppError::from_wallet_error)?;
765 Ok(Json(result))
766}
767
768pub async fn get_header_for_height(
774 State(wallet): State<WalletState>,
775 headers: HeaderMap,
776 Json(args): Json<GetHeaderArgs>,
777) -> Result<Json<GetHeaderResult>, AppError> {
778 let originator = extract_originator(&headers)?;
779 let result = wallet
780 .get_header_for_height(args, &originator)
781 .await
782 .map_err(AppError::from_wallet_error)?;
783 Ok(Json(result))
784}
785
786pub async fn verify_signature(
792 State(wallet): State<WalletState>,
793 headers: HeaderMap,
794 Json(req): Json<McVerifySignatureReq>,
795) -> Result<Json<serde_json::Value>, AppError> {
796 let originator = extract_originator(&headers)?;
797 let args = VerifySignatureArgs {
798 data: req.data,
799 hash_to_directly_verify: None,
800 signature: req.signature,
801 protocol_id: parse_protocol_id(&req.protocol_id)?,
802 key_id: req.key_id,
803 counterparty: req
804 .counterparty
805 .map(|s| parse_counterparty(&s))
806 .transpose()?,
807 for_self: req.for_self,
808 };
809 let result = wallet
810 .verify_signature(args, &originator)
811 .await
812 .map_err(AppError::from_wallet_error)?;
813 Ok(Json(json!({ "valid": result.valid })))
814}
815
816pub async fn encrypt(
818 State(wallet): State<WalletState>,
819 headers: HeaderMap,
820 Json(req): Json<McEncryptReq>,
821) -> Result<Json<serde_json::Value>, AppError> {
822 let originator = extract_originator(&headers)?;
823 let args = EncryptArgs {
824 plaintext: req.plaintext,
825 protocol_id: parse_protocol_id(&req.protocol_id)?,
826 key_id: req.key_id,
827 counterparty: req
828 .counterparty
829 .map(|s| parse_counterparty(&s))
830 .transpose()?,
831 };
832 audit::record(audit::protocol_entry(
833 "encrypt",
834 level_num(args.protocol_id.security_level),
835 &args.protocol_id.protocol_name,
836 args.counterparty.as_ref().map(cp_str),
837 Some(args.key_id.clone()),
838 &originator,
839 ));
840 let result = wallet
841 .encrypt(args, &originator)
842 .await
843 .map_err(AppError::from_wallet_error)?;
844 Ok(Json(json!({ "ciphertext": result.ciphertext })))
845}
846
847pub async fn decrypt(
849 State(wallet): State<WalletState>,
850 headers: HeaderMap,
851 Json(req): Json<McDecryptReq>,
852) -> Result<Json<serde_json::Value>, AppError> {
853 let originator = extract_originator(&headers)?;
854 let args = DecryptArgs {
855 ciphertext: req.ciphertext,
856 protocol_id: parse_protocol_id(&req.protocol_id)?,
857 key_id: req.key_id,
858 counterparty: req
859 .counterparty
860 .map(|s| parse_counterparty(&s))
861 .transpose()?,
862 };
863 audit::record(audit::protocol_entry(
864 "decrypt",
865 level_num(args.protocol_id.security_level),
866 &args.protocol_id.protocol_name,
867 args.counterparty.as_ref().map(cp_str),
868 Some(args.key_id.clone()),
869 &originator,
870 ));
871 let result = wallet
872 .decrypt(args, &originator)
873 .await
874 .map_err(AppError::from_wallet_error)?;
875 Ok(Json(json!({ "plaintext": result.plaintext })))
876}
877
878pub async fn create_hmac(
880 State(wallet): State<WalletState>,
881 headers: HeaderMap,
882 Json(req): Json<McCreateHmacReq>,
883) -> Result<Json<serde_json::Value>, AppError> {
884 let originator = extract_originator(&headers)?;
885 let args = CreateHmacArgs {
886 data: req.data,
887 protocol_id: parse_protocol_id(&req.protocol_id)?,
888 key_id: req.key_id,
889 counterparty: req
890 .counterparty
891 .map(|s| parse_counterparty(&s))
892 .transpose()?,
893 };
894 audit::record(audit::protocol_entry(
895 "createHmac",
896 level_num(args.protocol_id.security_level),
897 &args.protocol_id.protocol_name,
898 args.counterparty.as_ref().map(cp_str),
899 Some(args.key_id.clone()),
900 &originator,
901 ));
902 let result = wallet
903 .create_hmac(args, &originator)
904 .await
905 .map_err(AppError::from_wallet_error)?;
906 Ok(Json(json!({ "hmac": result.hmac.to_vec() })))
907}
908
909pub async fn verify_hmac(
911 State(wallet): State<WalletState>,
912 headers: HeaderMap,
913 Json(req): Json<McVerifyHmacReq>,
914) -> Result<Json<serde_json::Value>, AppError> {
915 let originator = extract_originator(&headers)?;
916 let hmac: [u8; 32] = req
917 .hmac
918 .try_into()
919 .map_err(|_| anyhow::anyhow!("hmac must be exactly 32 bytes"))?;
920 let args = VerifyHmacArgs {
921 data: req.data,
922 hmac,
923 protocol_id: parse_protocol_id(&req.protocol_id)?,
924 key_id: req.key_id,
925 counterparty: req
926 .counterparty
927 .map(|s| parse_counterparty(&s))
928 .transpose()?,
929 };
930 let result = wallet
931 .verify_hmac(args, &originator)
932 .await
933 .map_err(AppError::from_wallet_error)?;
934 Ok(Json(json!({ "valid": result.valid })))
935}
936
937pub async fn sign_action(
943 State(wallet): State<WalletState>,
944 headers: HeaderMap,
945 Json(args): Json<SignActionArgs>,
946) -> Result<Json<McSignActionRes>, AppError> {
947 let originator = extract_originator(&headers)?;
948 let reference = args.reference.clone();
949 let result = wallet
950 .sign_action(args, &originator)
951 .await
952 .map_err(AppError::from_wallet_error)?;
953 tracing::info!(originator = %originator, reference = %reference, "signAction");
954 if let Some(t) = result.txid.as_ref() {
958 if let Err(e) =
959 crate::spend_gate::fill_txid(wallet.storage().pool(), &reference, &to_hex(t)).await
960 {
961 tracing::error!(reference = %reference, error = %e, "signAction: the rule's txid not recorded");
962 }
963 }
964 let broadcast = match result.txid.as_ref() {
965 Some(t) if super::broadcast_follow_up::sending(&result.send_with_results, t) => {
966 super::post_word::post_word(wallet.storage(), &to_hex(t)).await
967 }
968 _ => None,
969 };
970 Ok(Json(McSignActionRes {
974 txid: result.txid.map(|t| to_hex(&t)),
975 tx: result.tx,
976 send_with_results: result
977 .send_with_results
978 .and_then(|r| serde_json::to_value(r).ok()),
979 broadcast,
980 }))
981}
982
983pub async fn abort_action(
985 State(wallet): State<WalletState>,
986 headers: HeaderMap,
987 Json(args): Json<AbortActionArgs>,
988) -> Result<Json<AbortActionResult>, AppError> {
989 let originator = extract_originator(&headers)?;
990 if let Some(txid) =
995 crate::retire_guard::abort_refused_for(wallet.storage().pool(), &args.reference)
996 .await
997 .map_err(AppError::from_wallet_error)?
998 {
999 return Err(AppError::from_wallet_error(format!(
1000 "Invalid operation: a broadcaster took transaction {txid}; it is not aborted. It stays, \
1001 re-asked, until a proof, a node verdict or a competitor's checked proof decides it"
1002 )));
1003 }
1004 let result = wallet.abort_action(args, &originator).await.map_err(|e| {
1005 let mut err = AppError::from_wallet_error(e);
1012 if err.message().contains("the network has transaction") {
1013 err = err.with_hint(
1014 "the wallet holds chain evidence for this transaction, so it \
1015 cannot be aborted; if it is a phantom after all, \
1016 `bsv-wallet reconcile-broadcasts --execute` re-checks it \
1017 against the chain index",
1018 );
1019 }
1020 err
1021 })?;
1022 Ok(Json(result))
1023}
1024
1025pub async fn list_actions(
1027 State(wallet): State<WalletState>,
1028 headers: HeaderMap,
1029 Json(args): Json<ListActionsArgs>,
1030) -> Result<Json<serde_json::Value>, AppError> {
1031 let originator = extract_originator(&headers)?;
1032 let result = wallet
1033 .list_actions(args, &originator)
1034 .await
1035 .map_err(AppError::from_wallet_error)?;
1036 let rules = crate::spend_gate::action_rules(wallet.storage().pool())
1037 .await
1038 .map_err(|e| AppError::from_wallet_error(format!("Storage error: {e}")))?;
1039 let result = serde_json::to_value(result)
1040 .map_err(|e| AppError::from_wallet_error(format!("Internal error: {e}")))?;
1041 Ok(Json(with_rules(result, &rules)))
1042}
1043
1044pub fn with_rules(
1048 mut actions: serde_json::Value,
1049 rules: &crate::spend_gate::ActionRules,
1050) -> serde_json::Value {
1051 if let Some(list) = actions.get_mut("actions").and_then(|a| a.as_array_mut()) {
1052 for action in list {
1053 let txid = action
1054 .get("txid")
1055 .and_then(|t| t.as_str())
1056 .map(str::to_ascii_lowercase);
1057 let rule = match txid {
1058 Some(t) if t.bytes().all(|b| b == b'0') => {
1059 let key = crate::spend_gate::template_key(
1060 action.get("status").and_then(|v| v.as_str()).unwrap_or(""),
1061 action
1062 .get("description")
1063 .and_then(|v| v.as_str())
1064 .unwrap_or(""),
1065 action.get("satoshis").and_then(|v| v.as_i64()).unwrap_or(0),
1066 );
1067 rules.by_template.get(&key)
1068 }
1069 Some(t) => rules.by_txid.get(&t),
1070 None => None,
1071 }
1072 .cloned();
1073 if let (Some(rule), Some(obj)) = (rule, action.as_object_mut()) {
1074 obj.insert("underRule".into(), json!(rule));
1075 }
1076 }
1077 }
1078 actions
1079}
1080
1081pub async fn list_outputs(
1083 State(wallet): State<WalletState>,
1084 headers: HeaderMap,
1085 Json(args): Json<ListOutputsArgs>,
1086) -> Result<Json<ListOutputsResult>, AppError> {
1087 let originator = extract_originator(&headers)?;
1088 let result = wallet
1089 .list_outputs(args, &originator)
1090 .await
1091 .map_err(AppError::from_wallet_error)?;
1092 Ok(Json(result))
1093}
1094
1095pub async fn relinquish_output(
1100 State(wallet): State<WalletState>,
1101 axum::Extension(spending_lock): axum::Extension<super::SpendingLock>,
1102 headers: HeaderMap,
1103 Json(args): Json<RelinquishOutputArgs>,
1104) -> Result<Json<RelinquishOutputResult>, AppError> {
1105 let originator = extract_originator(&headers)?;
1106 let _guard = spending_lock.lock().await;
1107 let result = wallet
1108 .relinquish_output(args, &originator)
1109 .await
1110 .map_err(AppError::from_wallet_error)?;
1111 Ok(Json(result))
1112}
1113
1114pub async fn acquire_certificate(
1120 State(wallet): State<WalletState>,
1121 headers: HeaderMap,
1122 Json(args): Json<AcquireCertificateArgs>,
1123) -> Result<Json<WalletCertificate>, AppError> {
1124 let originator = extract_originator(&headers)?;
1125 let result = wallet
1126 .acquire_certificate(args, &originator)
1127 .await
1128 .map_err(AppError::from_wallet_error)?;
1129 Ok(Json(result))
1130}
1131
1132pub async fn list_certificates(
1134 State(wallet): State<WalletState>,
1135 headers: HeaderMap,
1136 Json(args): Json<ListCertificatesArgs>,
1137) -> Result<Json<ListCertificatesResult>, AppError> {
1138 let originator = extract_originator(&headers)?;
1139 let result = wallet
1140 .list_certificates(args, &originator)
1141 .await
1142 .map_err(AppError::from_wallet_error)?;
1143 Ok(Json(result))
1144}
1145
1146pub async fn prove_certificate(
1148 State(wallet): State<WalletState>,
1149 headers: HeaderMap,
1150 Json(args): Json<ProveCertificateArgs>,
1151) -> Result<Json<ProveCertificateResult>, AppError> {
1152 let originator = extract_originator(&headers)?;
1153 let result = wallet
1154 .prove_certificate(args, &originator)
1155 .await
1156 .map_err(AppError::from_wallet_error)?;
1157 Ok(Json(result))
1158}
1159
1160pub async fn relinquish_certificate(
1162 State(wallet): State<WalletState>,
1163 headers: HeaderMap,
1164 Json(args): Json<RelinquishCertificateArgs>,
1165) -> Result<Json<RelinquishCertificateResult>, AppError> {
1166 let originator = extract_originator(&headers)?;
1167 let result = wallet
1168 .relinquish_certificate(args, &originator)
1169 .await
1170 .map_err(AppError::from_wallet_error)?;
1171 Ok(Json(result))
1172}
1173
1174pub async fn discover_by_identity_key(
1180 State(wallet): State<WalletState>,
1181 headers: HeaderMap,
1182 Json(args): Json<DiscoverByIdentityKeyArgs>,
1183) -> Result<Json<DiscoverCertificatesResult>, AppError> {
1184 let originator = extract_originator(&headers)?;
1185 let result = wallet
1186 .discover_by_identity_key(args, &originator)
1187 .await
1188 .map_err(AppError::from_wallet_error)?;
1189 Ok(Json(result))
1190}
1191
1192pub async fn discover_by_attributes(
1194 State(wallet): State<WalletState>,
1195 headers: HeaderMap,
1196 Json(args): Json<DiscoverByAttributesArgs>,
1197) -> Result<Json<DiscoverCertificatesResult>, AppError> {
1198 let originator = extract_originator(&headers)?;
1199 let result = wallet
1200 .discover_by_attributes(args, &originator)
1201 .await
1202 .map_err(AppError::from_wallet_error)?;
1203 Ok(Json(result))
1204}
1205
1206pub async fn reveal_counterparty_key_linkage(
1208 State(wallet): State<WalletState>,
1209 headers: HeaderMap,
1210 Json(req): Json<McRevealCounterpartyKeyLinkageReq>,
1211) -> Result<Json<RevealCounterpartyKeyLinkageResult>, AppError> {
1212 let originator = extract_originator(&headers)?;
1213 let args = WalletRevealCounterpartyArgs {
1214 counterparty: PublicKey::from_hex(&req.counterparty)
1215 .map_err(|e| anyhow::anyhow!("{}", e))?,
1216 verifier: PublicKey::from_hex(&req.verifier).map_err(|e| anyhow::anyhow!("{}", e))?,
1217 privileged: req.privileged,
1218 privileged_reason: req.privileged_reason,
1219 };
1220 let result = wallet
1221 .reveal_counterparty_key_linkage(args, &originator)
1222 .await
1223 .map_err(AppError::from_wallet_error)?;
1224 Ok(Json(result))
1225}
1226
1227pub async fn reveal_specific_key_linkage(
1229 State(wallet): State<WalletState>,
1230 headers: HeaderMap,
1231 Json(req): Json<McRevealSpecificKeyLinkageReq>,
1232) -> Result<Json<RevealSpecificKeyLinkageResult>, AppError> {
1233 let originator = extract_originator(&headers)?;
1234 let args = WalletRevealSpecificArgs {
1235 counterparty: parse_counterparty(&req.counterparty)?,
1236 verifier: PublicKey::from_hex(&req.verifier).map_err(|e| anyhow::anyhow!("{}", e))?,
1237 protocol_id: parse_protocol_id(&req.protocol_id)?,
1238 key_id: req.key_id,
1239 privileged: req.privileged,
1240 privileged_reason: req.privileged_reason,
1241 };
1242 let result = wallet
1243 .reveal_specific_key_linkage(args, &originator)
1244 .await
1245 .map_err(AppError::from_wallet_error)?;
1246 Ok(Json(result))
1247}