1use std::collections::HashMap;
4use std::str::FromStr;
5
6use compact_str::CompactString;
7use rust_decimal::Decimal;
8use serde::{Deserialize, Serialize};
9use serde_with::{VecSkipError, serde_as};
10use thiserror::Error;
11
12use super::{Base64Bytes, Extensions, Version, Version2};
13use crate::chain::ChainId;
14use crate::error::{AsPaymentProblem, ErrorReason, FacilitatorError, VerificationError};
15use crate::scheme::SchemeSlug;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct TypedVerifyRequest<const V: u8, TPayload, TRequirements> {
26 pub x402_version: Version<V>,
28 pub payment_payload: TPayload,
30 pub payment_requirements: TRequirements,
32}
33
34impl<const V: u8, TPayload, TRequirements> TypedVerifyRequest<V, TPayload, TRequirements>
35where
36 Self: serde::de::DeserializeOwned,
37{
38 pub fn from_verify(request: VerifyRequest) -> Result<Self, VerificationError> {
44 serde_json::from_value(request.into_json())
45 .map_err(|e| VerificationError::InvalidFormat(e.to_string()))
46 }
47
48 pub fn from_settle(request: SettleRequest) -> Result<Self, VerificationError> {
54 serde_json::from_value(request.into_json())
55 .map_err(|e| VerificationError::InvalidFormat(e.to_string()))
56 }
57}
58
59impl<const V: u8, TPayload, TRequirements> TryFrom<TypedVerifyRequest<V, TPayload, TRequirements>>
60 for VerifyRequest
61where
62 TPayload: Serialize,
63 TRequirements: Serialize,
64{
65 type Error = serde_json::Error;
66 fn try_from(
67 value: TypedVerifyRequest<V, TPayload, TRequirements>,
68 ) -> Result<Self, Self::Error> {
69 let json = serde_json::to_value(value)?;
70 Ok(Self(json))
71 }
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct VerifyRequest(serde_json::Value);
77
78impl VerifyRequest {
79 #[must_use]
81 pub fn into_json(self) -> serde_json::Value {
82 self.0
83 }
84
85 #[must_use]
87 pub fn scheme_slug(&self) -> Option<SchemeSlug> {
88 scheme_slug_from_json(&self.0)
89 }
90
91 #[must_use]
93 pub fn network(&self) -> &str {
94 network_from_json(&self.0)
95 }
96}
97
98impl From<serde_json::Value> for VerifyRequest {
99 fn from(value: serde_json::Value) -> Self {
100 Self(value)
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct SettleRequest(serde_json::Value);
108
109impl SettleRequest {
110 #[must_use]
112 pub fn into_json(self) -> serde_json::Value {
113 self.0
114 }
115
116 #[must_use]
118 pub fn scheme_slug(&self) -> Option<SchemeSlug> {
119 scheme_slug_from_json(&self.0)
120 }
121
122 #[must_use]
124 pub fn network(&self) -> &str {
125 network_from_json(&self.0)
126 }
127
128 pub fn set_settlement_amount(&mut self, amount: &str) -> Result<(), VerificationError> {
140 let req = self
141 .0
142 .get_mut("paymentRequirements")
143 .and_then(serde_json::Value::as_object_mut)
144 .ok_or_else(|| {
145 VerificationError::InvalidFormat(
146 "settle request missing paymentRequirements object".into(),
147 )
148 })?;
149 let _ = req.insert(
150 "amount".to_owned(),
151 serde_json::Value::String(amount.to_owned()),
152 );
153 Ok(())
154 }
155}
156
157impl From<serde_json::Value> for SettleRequest {
158 fn from(value: serde_json::Value) -> Self {
159 Self(value)
160 }
161}
162
163impl From<VerifyRequest> for SettleRequest {
164 fn from(request: VerifyRequest) -> Self {
165 Self(request.into_json())
166 }
167}
168
169fn scheme_slug_from_json(json: &serde_json::Value) -> Option<SchemeSlug> {
170 let version = json.get("x402Version")?.as_u64()?;
171 let version: u8 = version.try_into().ok()?;
172 if version != Version2::VALUE {
173 return None;
174 }
175 let accepted = json.get("paymentPayload")?.get("accepted")?;
176 let chain_id = ChainId::from_str(accepted.get("network")?.as_str()?).ok()?;
177 let scheme = accepted.get("scheme")?.as_str()?;
178 Some(SchemeSlug::new(chain_id, scheme.into()))
179}
180
181fn network_from_json(json: &serde_json::Value) -> &str {
182 json.get("paymentRequirements")
183 .and_then(|r| r.get("network"))
184 .and_then(serde_json::Value::as_str)
185 .unwrap_or_default()
186}
187
188#[cfg(test)]
189mod request_tests {
190 use super::*;
191
192 fn v2_json(network: &str, scheme: &str) -> serde_json::Value {
193 serde_json::json!({
194 "x402Version": 2,
195 "paymentPayload": {
196 "accepted": { "network": network, "scheme": scheme }
197 },
198 "paymentRequirements": { "network": network }
199 })
200 }
201
202 #[test]
203 fn verify_request_scheme_slug_evm() {
204 let req = VerifyRequest::from(v2_json("eip155:8453", "exact"));
205 let slug = req.scheme_slug().unwrap();
206 assert_eq!(slug.to_string(), "eip155:8453:exact");
207 }
208
209 #[test]
210 fn settle_request_from_verify_preserves_slug() {
211 let verify = VerifyRequest::from(v2_json("eip155:42161", "exact"));
212 let settle: SettleRequest = verify.into();
213 assert_eq!(
214 settle.scheme_slug().unwrap().to_string(),
215 "eip155:42161:exact"
216 );
217 }
218
219 #[test]
220 fn settle_request_network_missing_returns_empty() {
221 let settle = SettleRequest::from(serde_json::json!({}));
222 assert_eq!(settle.network(), "");
223 }
224
225 #[test]
226 fn slug_rejects_wrong_version() {
227 let mut json = v2_json("eip155:1", "exact");
228 json["x402Version"] = serde_json::json!(99);
229 assert!(scheme_slug_from_json(&json).is_none());
230 }
231
232 #[test]
233 fn slug_rejects_invalid_caip2() {
234 assert!(scheme_slug_from_json(&v2_json("not-a-caip2", "exact")).is_none());
235 }
236
237 #[test]
238 fn settle_amount_override_rewrites_payment_requirements() {
239 let mut settle = SettleRequest::from(serde_json::json!({
240 "x402Version": 2,
241 "paymentPayload": { "accepted": { "network": "eip155:8453", "scheme": "upto" } },
242 "paymentRequirements": { "network": "eip155:8453", "amount": "5000000" }
243 }));
244 settle.set_settlement_amount("1500000").unwrap();
245 let json = settle.into_json();
246 assert_eq!(
247 json["paymentRequirements"]["amount"].as_str(),
248 Some("1500000")
249 );
250 }
251
252 #[test]
253 fn settle_amount_override_errors_when_requirements_missing() {
254 let mut settle = SettleRequest::from(serde_json::json!({}));
255 let err = settle.set_settlement_amount("1").unwrap_err();
256 assert!(matches!(err, VerificationError::InvalidFormat(_)));
257 }
258}
259
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
265#[serde(into = "VerifyResponseWire", try_from = "VerifyResponseWire")]
266#[non_exhaustive]
267pub enum VerifyResponse {
268 Valid {
270 payer: CompactString,
272 extensions: Extensions,
274 },
275 Invalid {
277 reason: ErrorReason,
279 message: Option<CompactString>,
281 payer: Option<CompactString>,
283 extensions: Extensions,
285 },
286}
287
288impl VerifyResponse {
289 #[must_use]
291 pub fn valid(payer: impl Into<CompactString>) -> Self {
292 Self::Valid {
293 payer: payer.into(),
294 extensions: Extensions::new(),
295 }
296 }
297
298 #[must_use]
300 pub fn invalid(payer: Option<CompactString>, reason: ErrorReason) -> Self {
301 Self::Invalid {
302 reason,
303 message: None,
304 payer,
305 extensions: Extensions::new(),
306 }
307 }
308
309 #[must_use]
311 pub fn invalid_with_message(
312 payer: Option<CompactString>,
313 reason: ErrorReason,
314 message: impl Into<CompactString>,
315 ) -> Self {
316 Self::Invalid {
317 reason,
318 message: Some(message.into()),
319 payer,
320 extensions: Extensions::new(),
321 }
322 }
323
324 #[must_use]
326 pub const fn is_valid(&self) -> bool {
327 matches!(self, Self::Valid { .. })
328 }
329
330 #[must_use]
333 pub fn from_facilitator_error(error: &FacilitatorError) -> Self {
334 let problem = error.as_payment_problem();
335 Self::Invalid {
336 reason: problem.reason(),
337 message: Some(CompactString::from(problem.details())),
338 payer: None,
339 extensions: Extensions::new(),
340 }
341 }
342}
343
344#[derive(Serialize, Deserialize)]
346#[serde(rename_all = "camelCase", deny_unknown_fields)]
347struct VerifyResponseWire {
348 is_valid: bool,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
350 payer: Option<CompactString>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
352 invalid_reason: Option<ErrorReason>,
353 #[serde(default, skip_serializing_if = "Option::is_none")]
354 invalid_message: Option<CompactString>,
355 #[serde(default, skip_serializing_if = "Extensions::is_empty")]
356 extensions: Extensions,
357}
358
359impl From<VerifyResponse> for VerifyResponseWire {
360 fn from(value: VerifyResponse) -> Self {
361 match value {
362 VerifyResponse::Valid { payer, extensions } => Self {
363 is_valid: true,
364 payer: Some(payer),
365 invalid_reason: None,
366 invalid_message: None,
367 extensions,
368 },
369 VerifyResponse::Invalid {
370 reason,
371 message,
372 payer,
373 extensions,
374 } => Self {
375 is_valid: false,
376 payer,
377 invalid_reason: Some(reason),
378 invalid_message: message,
379 extensions,
380 },
381 }
382 }
383}
384
385impl TryFrom<VerifyResponseWire> for VerifyResponse {
386 type Error = String;
387 fn try_from(wire: VerifyResponseWire) -> Result<Self, Self::Error> {
388 if wire.is_valid {
389 Ok(Self::Valid {
390 payer: wire.payer.ok_or("missing field: payer")?,
391 extensions: wire.extensions,
392 })
393 } else {
394 Ok(Self::Invalid {
395 reason: wire.invalid_reason.ok_or("missing field: invalidReason")?,
396 message: wire.invalid_message,
397 payer: wire.payer,
398 extensions: wire.extensions,
399 })
400 }
401 }
402}
403
404#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
406#[serde(into = "SettleResponseWire", try_from = "SettleResponseWire")]
407#[non_exhaustive]
408pub enum SettleResponse {
409 Success {
411 payer: CompactString,
413 transaction: CompactString,
415 network: CompactString,
417 #[serde(default, skip_serializing_if = "Option::is_none")]
422 amount: Option<CompactString>,
423 extensions: Extensions,
425 },
426 Failure {
428 reason: ErrorReason,
430 message: Option<CompactString>,
432 payer: Option<CompactString>,
434 network: CompactString,
436 extensions: Extensions,
438 },
439}
440
441impl SettleResponse {
442 #[must_use]
444 pub const fn is_success(&self) -> bool {
445 matches!(self, Self::Success { .. })
446 }
447
448 #[must_use]
457 pub fn encode_base64(&self) -> Option<Base64Bytes> {
458 if !self.is_success() {
459 return None;
460 }
461 let json = serde_json::to_vec(self).ok()?;
462 Some(Base64Bytes::encode(json))
463 }
464
465 #[must_use]
474 pub fn encode_base64_any(&self) -> Option<Base64Bytes> {
475 let json = serde_json::to_vec(self).ok()?;
476 Some(Base64Bytes::encode(json))
477 }
478
479 #[must_use]
481 pub fn from_facilitator_error(
482 error: &FacilitatorError,
483 network: impl Into<CompactString>,
484 ) -> Self {
485 let problem = error.as_payment_problem();
486 Self::Failure {
487 reason: problem.reason(),
488 message: Some(CompactString::from(problem.details())),
489 payer: None,
490 network: network.into(),
491 extensions: Extensions::new(),
492 }
493 }
494}
495
496#[derive(Serialize, Deserialize)]
498#[serde(rename_all = "camelCase", deny_unknown_fields)]
499struct SettleResponseWire {
500 success: bool,
501 #[serde(default, skip_serializing_if = "Option::is_none")]
502 error_reason: Option<ErrorReason>,
503 #[serde(default, skip_serializing_if = "Option::is_none")]
504 error_message: Option<CompactString>,
505 #[serde(default, skip_serializing_if = "Option::is_none")]
506 payer: Option<CompactString>,
507 #[serde(default)]
511 transaction: CompactString,
512 network: CompactString,
513 #[serde(default, skip_serializing_if = "Option::is_none")]
514 amount: Option<CompactString>,
515 #[serde(default, skip_serializing_if = "Extensions::is_empty")]
516 extensions: Extensions,
517}
518
519impl From<SettleResponse> for SettleResponseWire {
520 fn from(value: SettleResponse) -> Self {
521 match value {
522 SettleResponse::Success {
523 payer,
524 transaction,
525 network,
526 amount,
527 extensions,
528 } => Self {
529 success: true,
530 error_reason: None,
531 error_message: None,
532 payer: Some(payer),
533 transaction,
534 network,
535 amount,
536 extensions,
537 },
538 SettleResponse::Failure {
539 reason,
540 message,
541 payer,
542 network,
543 extensions,
544 } => Self {
545 success: false,
546 error_reason: Some(reason),
547 error_message: message,
548 payer,
549 transaction: CompactString::default(),
550 network,
551 amount: None,
552 extensions,
553 },
554 }
555 }
556}
557
558impl TryFrom<SettleResponseWire> for SettleResponse {
559 type Error = String;
560 fn try_from(wire: SettleResponseWire) -> Result<Self, Self::Error> {
561 if wire.success {
562 let payer = wire.payer.ok_or("missing field: payer")?;
563 Ok(Self::Success {
564 payer,
565 transaction: wire.transaction,
566 network: wire.network,
567 amount: wire.amount,
568 extensions: wire.extensions,
569 })
570 } else {
571 Ok(Self::Failure {
572 reason: wire.error_reason.ok_or("missing field: errorReason")?,
573 message: wire.error_message,
574 payer: wire.payer,
575 network: wire.network,
576 extensions: wire.extensions,
577 })
578 }
579 }
580}
581
582#[cfg(test)]
583mod response_tests {
584 use serde_json::json;
585
586 use super::*;
587 use crate::wire::ExtensionEntry;
588
589 #[test]
590 fn verify_valid_roundtrip() {
591 let response = VerifyResponse::valid("0xABC");
592 let encoded = serde_json::to_value(&response).unwrap();
593 assert_eq!(encoded["isValid"], true);
594 assert_eq!(encoded["payer"], "0xABC");
595 assert!(encoded.get("invalidReason").is_none());
596
597 let back: VerifyResponse = serde_json::from_value(encoded).unwrap();
598 assert_eq!(back, response);
599 }
600
601 #[test]
602 fn verify_valid_with_extensions_roundtrip() {
603 let mut extensions = Extensions::new();
604 extensions.insert(
605 "payment-identifier",
606 ExtensionEntry::info(json!({"id": "order-123"})),
607 );
608 let response = VerifyResponse::Valid {
609 payer: "0xABC".into(),
610 extensions,
611 };
612 let encoded = serde_json::to_value(&response).unwrap();
613 assert_eq!(
614 encoded["extensions"]["payment-identifier"]["info"]["id"],
615 "order-123"
616 );
617 let back: VerifyResponse = serde_json::from_value(encoded).unwrap();
618 assert_eq!(back, response);
619 }
620
621 #[test]
622 fn verify_invalid_roundtrip() {
623 let response = VerifyResponse::invalid_with_message(
624 Some("0xDEF".into()),
625 ErrorReason::InsufficientFunds,
626 "not enough USDC",
627 );
628 let encoded = serde_json::to_value(&response).unwrap();
629 assert_eq!(encoded["isValid"], false);
630 assert_eq!(encoded["invalidReason"], "insufficient_funds");
631 assert_eq!(encoded["invalidMessage"], "not enough USDC");
632 let back: VerifyResponse = serde_json::from_value(encoded).unwrap();
633 assert_eq!(back, response);
634 }
635
636 #[test]
637 fn settle_success_with_amount_roundtrip() {
638 let response = SettleResponse::Success {
639 payer: "0xABC".into(),
640 transaction: "0xTX".into(),
641 network: "eip155:8453".into(),
642 amount: Some("1000000".into()),
643 extensions: Extensions::new(),
644 };
645 let encoded = serde_json::to_value(&response).unwrap();
646 assert_eq!(encoded["success"], true);
647 assert_eq!(encoded["amount"], "1000000");
648 let back: SettleResponse = serde_json::from_value(encoded).unwrap();
649 assert_eq!(back, response);
650 }
651
652 #[test]
653 fn settle_success_without_amount_is_valid() {
654 let response = SettleResponse::Success {
655 payer: "0xABC".into(),
656 transaction: "0xTX".into(),
657 network: "eip155:1".into(),
658 amount: None,
659 extensions: Extensions::new(),
660 };
661 let encoded = serde_json::to_value(&response).unwrap();
662 assert!(encoded.get("amount").is_none());
663 let back: SettleResponse = serde_json::from_value(encoded).unwrap();
664 assert_eq!(back, response);
665 }
666
667 #[test]
668 fn settle_success_empty_transaction_allowed() {
669 let json = json!({
671 "success": true,
672 "payer": "0xABC",
673 "transaction": "",
674 "network": "eip155:1"
675 });
676 let back: SettleResponse = serde_json::from_value(json).unwrap();
677 assert!(matches!(
678 back,
679 SettleResponse::Success { ref transaction, .. } if transaction.is_empty()
680 ));
681 }
682
683 #[test]
684 fn settle_failure_roundtrip() {
685 let response = SettleResponse::Failure {
686 reason: ErrorReason::DuplicateSettlement,
687 message: Some("already processed".into()),
688 payer: None,
689 network: "solana:mainnet".into(),
690 extensions: Extensions::new(),
691 };
692 let encoded = serde_json::to_value(&response).unwrap();
693 assert_eq!(encoded["errorReason"], "duplicate_settlement");
694 let back: SettleResponse = serde_json::from_value(encoded).unwrap();
695 assert_eq!(back, response);
696 }
697
698 #[test]
701 fn settle_failure_serializes_empty_transaction() {
702 let response = SettleResponse::Failure {
703 reason: ErrorReason::UnexpectedSettleError,
704 message: None,
705 payer: None,
706 network: "eip155:8453".into(),
707 extensions: Extensions::new(),
708 };
709 let encoded = serde_json::to_value(&response).unwrap();
710 assert_eq!(
711 encoded["transaction"], "",
712 "spec §5.3.2 requires `transaction` field present (empty on failure)"
713 );
714 }
715
716 #[test]
717 fn settle_encode_base64_only_for_success() {
718 let success = SettleResponse::Success {
719 payer: "0xA".into(),
720 transaction: "0xT".into(),
721 network: "eip155:1".into(),
722 amount: None,
723 extensions: Extensions::new(),
724 };
725 assert!(success.encode_base64().is_some());
726
727 let failure = SettleResponse::Failure {
728 reason: ErrorReason::UnexpectedSettleError,
729 message: None,
730 payer: None,
731 network: "eip155:1".into(),
732 extensions: Extensions::new(),
733 };
734 assert!(failure.encode_base64().is_none());
735 }
736}
737
738#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
740#[serde(rename_all = "camelCase", deny_unknown_fields)]
741#[non_exhaustive]
742pub struct SupportedPaymentKind {
743 pub x402_version: u8,
745 pub scheme: CompactString,
747 pub network: CompactString,
749 #[serde(default, skip_serializing_if = "Option::is_none")]
751 pub extra: Option<serde_json::Value>,
752}
753
754impl SupportedPaymentKind {
755 #[must_use]
758 pub fn new(
759 x402_version: u8,
760 scheme: impl Into<CompactString>,
761 network: impl Into<CompactString>,
762 ) -> Self {
763 Self {
764 x402_version,
765 scheme: scheme.into(),
766 network: network.into(),
767 extra: None,
768 }
769 }
770
771 #[must_use]
773 pub fn with_extra(mut self, extra: serde_json::Value) -> Self {
774 self.extra = Some(extra);
775 self
776 }
777
778 #[must_use]
781 pub fn with_optional_extra(mut self, extra: Option<serde_json::Value>) -> Self {
782 self.extra = extra;
783 self
784 }
785}
786
787#[serde_as]
792#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
793#[serde(rename_all = "camelCase", deny_unknown_fields)]
794#[non_exhaustive]
795pub struct SupportedResponse {
796 #[serde_as(as = "VecSkipError<_>")]
798 pub kinds: Vec<SupportedPaymentKind>,
799 #[serde(default)]
801 pub extensions: Vec<CompactString>,
802 #[serde(default)]
805 pub signers: HashMap<CompactString, Vec<CompactString>>,
806}
807
808impl SupportedResponse {
809 #[must_use]
813 pub fn new() -> Self {
814 Self::default()
815 }
816
817 #[must_use]
819 pub fn with_kinds(mut self, kinds: Vec<SupportedPaymentKind>) -> Self {
820 self.kinds = kinds;
821 self
822 }
823
824 #[must_use]
826 pub fn with_extensions(mut self, extensions: Vec<CompactString>) -> Self {
827 self.extensions = extensions;
828 self
829 }
830
831 #[must_use]
833 pub fn with_signers(mut self, signers: HashMap<CompactString, Vec<CompactString>>) -> Self {
834 self.signers = signers;
835 self
836 }
837
838 #[must_use]
843 pub fn signers_for_chain(&self, chain_id: &ChainId) -> Vec<&str> {
844 let exact = CompactString::from(chain_id.to_string());
845 let wildcard = CompactString::from(format!("{}:*", chain_id.namespace()));
846 let mut out = Vec::new();
847 if let Some(list) = self.signers.get(&exact) {
848 out.extend(list.iter().map(CompactString::as_str));
849 }
850 if let Some(list) = self.signers.get(&wildcard) {
851 out.extend(list.iter().map(CompactString::as_str));
852 }
853 out
854 }
855}
856
857#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
859#[serde(rename_all = "camelCase")]
860#[non_exhaustive]
861pub struct SettlementOverrides {
862 #[serde(default, skip_serializing_if = "Option::is_none")]
864 pub amount: Option<String>,
865}
866
867impl SettlementOverrides {
868 #[must_use]
870 pub fn amount(amount: impl Into<String>) -> Self {
871 Self {
872 amount: Some(amount.into()),
873 }
874 }
875}
876
877#[derive(Debug, Clone, PartialEq, Eq, Error)]
879pub enum SettlementOverrideError {
880 #[error("invalid requirements amount: {0}")]
882 InvalidRequirementsAmount(String),
883 #[error("invalid settlement override amount: {0}")]
885 InvalidOverride(String),
886 #[error("settlement override arithmetic overflow")]
888 Overflow,
889}
890
891pub const DEFAULT_ASSET_DECIMALS: u32 = 6;
893
894#[must_use]
898pub fn asset_decimals_from_extra(extra: Option<&serde_json::Value>) -> u32 {
899 let Some(extra) = extra else {
900 return DEFAULT_ASSET_DECIMALS;
901 };
902 match extra.get("decimals") {
903 Some(serde_json::Value::Number(n)) => n
904 .as_u64()
905 .and_then(|v| u32::try_from(v).ok())
906 .unwrap_or(DEFAULT_ASSET_DECIMALS),
907 Some(serde_json::Value::String(s)) => s.parse().unwrap_or(DEFAULT_ASSET_DECIMALS),
908 _ => DEFAULT_ASSET_DECIMALS,
909 }
910}
911
912pub fn resolve_settlement_override_amount(
919 raw_amount: &str,
920 authorized_max: &str,
921 decimals: u32,
922) -> Result<String, SettlementOverrideError> {
923 let raw = raw_amount.trim();
924 if let Some(percent_body) = raw.strip_suffix('%') {
925 return resolve_percent(percent_body.trim(), authorized_max);
926 }
927 if let Some(dollar_body) = raw.strip_prefix('$') {
928 return resolve_dollar(dollar_body.trim(), decimals);
929 }
930 if raw.is_empty() || !raw.bytes().all(|b| b.is_ascii_digit()) {
932 return Err(SettlementOverrideError::InvalidOverride(raw.to_owned()));
933 }
934 Ok(raw.to_owned())
935}
936
937fn resolve_percent(percent: &str, authorized_max: &str) -> Result<String, SettlementOverrideError> {
938 let (int_part, frac_part) = match percent.split_once('.') {
940 Some((i, f)) => (i, f),
941 None => (percent, ""),
942 };
943 if int_part.is_empty()
944 || !int_part.bytes().all(|b| b.is_ascii_digit())
945 || frac_part.len() > 2
946 || !frac_part.bytes().all(|b| b.is_ascii_digit())
947 {
948 return Err(SettlementOverrideError::InvalidOverride(format!(
949 "{percent}%"
950 )));
951 }
952 let int_val: u128 = int_part
953 .parse()
954 .map_err(|_| SettlementOverrideError::InvalidOverride(format!("{percent}%")))?;
955 let frac_padded = format!("{frac_part:0<2}");
956 let frac_val: u128 = if frac_padded.is_empty() {
957 0
958 } else {
959 frac_padded
960 .parse()
961 .map_err(|_| SettlementOverrideError::InvalidOverride(format!("{percent}%")))?
962 };
963 let scaled_percent = int_val
965 .checked_mul(100)
966 .and_then(|v| v.checked_add(frac_val))
967 .ok_or(SettlementOverrideError::Overflow)?;
968
969 let base = parse_u128_amount(authorized_max)?;
970 let product = base
972 .checked_mul(scaled_percent)
973 .ok_or(SettlementOverrideError::Overflow)?;
974 Ok((product / 10_000).to_string())
975}
976
977fn resolve_dollar(dollar: &str, decimals: u32) -> Result<String, SettlementOverrideError> {
978 let dollar_dec = Decimal::from_str(dollar)
979 .map_err(|_| SettlementOverrideError::InvalidOverride(format!("${dollar}")))?;
980 if dollar_dec.is_sign_negative() {
981 return Err(SettlementOverrideError::InvalidOverride(format!(
982 "${dollar}"
983 )));
984 }
985 let scale = Decimal::from(10u64.pow(decimals.min(18)));
986 let atomic = dollar_dec
987 .checked_mul(scale)
988 .ok_or(SettlementOverrideError::Overflow)?;
989 let truncated = atomic.trunc();
991 let s = truncated.normalize().to_string();
992 Ok(s.split('.').next().unwrap_or("0").to_owned())
994}
995
996fn parse_u128_amount(raw: &str) -> Result<u128, SettlementOverrideError> {
997 if raw.is_empty() || !raw.bytes().all(|b| b.is_ascii_digit()) {
998 return Err(SettlementOverrideError::InvalidRequirementsAmount(
999 raw.to_owned(),
1000 ));
1001 }
1002 raw.parse()
1003 .map_err(|_| SettlementOverrideError::InvalidRequirementsAmount(raw.to_owned()))
1004}
1005
1006#[cfg(test)]
1007mod override_tests {
1008 use super::*;
1009
1010 #[test]
1011 fn atomic_passthrough() {
1012 assert_eq!(
1013 resolve_settlement_override_amount("500", "1000000", 6).unwrap(),
1014 "500"
1015 );
1016 }
1017
1018 #[test]
1019 fn percent_half() {
1020 assert_eq!(
1021 resolve_settlement_override_amount("50%", "1000000", 6).unwrap(),
1022 "500000"
1023 );
1024 }
1025
1026 #[test]
1027 fn percent_with_fraction() {
1028 assert_eq!(
1030 resolve_settlement_override_amount("12.5%", "1000", 6).unwrap(),
1031 "125"
1032 );
1033 }
1034
1035 #[test]
1036 fn dollar_default_decimals() {
1037 assert_eq!(
1038 resolve_settlement_override_amount("$0.001", "1000000", 6).unwrap(),
1039 "1000"
1040 );
1041 }
1042
1043 #[test]
1044 fn rejects_empty_atomic() {
1045 assert!(resolve_settlement_override_amount("", "1", 6).is_err());
1046 }
1047
1048 #[test]
1049 fn decimals_from_extra() {
1050 let v = serde_json::json!({"decimals": 18});
1051 assert_eq!(asset_decimals_from_extra(Some(&v)), 18);
1052 assert_eq!(asset_decimals_from_extra(None), 6);
1053 }
1054}