Skip to main content

r402_protocol/
scheme.rs

1//! Scheme identifiers and name-marker types.
2//!
3//! - [`ExactScheme`] — transfer of at least `amount`
4//! - [`UptoScheme`] — buyer authorises a maximum; settle may charge less
5//! - [`BatchSettlementScheme`] — deferred / channel-backed settlement
6//! - [`AuthCaptureScheme`] — authorize / capture / void / refund
7
8use std::fmt::{self, Debug, Display, Formatter};
9
10use compact_str::CompactString;
11
12use crate::network::ChainId;
13
14/// Identity for scheme markers and chain-crate handlers.
15pub trait SchemeId {
16    /// CAIP-2 namespace (e.g. `"eip155"`, `"solana"`).
17    fn namespace(&self) -> &str;
18    /// Scheme name (e.g. `"exact"`, `"upto"`).
19    fn scheme(&self) -> &str;
20    /// CAIP-2 family pattern — defaults to `"{namespace}:*"`.
21    fn caip_family(&self) -> String {
22        format!("{}:*", self.namespace())
23    }
24    /// Human-readable identifier — defaults to `"{namespace}-{scheme}"`.
25    fn id(&self) -> String {
26        format!("{}-{}", self.namespace(), self.scheme())
27    }
28}
29
30/// Unique identifier for a scheme handler (`chain` + scheme name).
31#[derive(Debug, PartialEq, Eq, Hash, Clone)]
32pub struct SchemeSlug {
33    /// Chain this handler operates on.
34    pub chain_id: ChainId,
35    /// Scheme name (`"exact"`, `"upto"`, …).
36    pub name: CompactString,
37}
38
39impl SchemeSlug {
40    /// Constructs a slug.
41    #[must_use]
42    pub const fn new(chain_id: ChainId, name: CompactString) -> Self {
43        Self { chain_id, name }
44    }
45
46    /// Wildcard copy with the reference replaced by `*`.
47    #[must_use]
48    pub fn as_wildcard(&self) -> Self {
49        Self {
50            chain_id: ChainId::new(self.chain_id.namespace(), "*"),
51            name: self.name.clone(),
52        }
53    }
54
55    /// Whether the reference is `*`.
56    #[must_use]
57    pub fn is_wildcard(&self) -> bool {
58        self.chain_id.reference() == "*"
59    }
60}
61
62impl Display for SchemeSlug {
63    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
64        write!(
65            f,
66            "{}:{}:{}",
67            self.chain_id.namespace(),
68            self.chain_id.reference(),
69            self.name,
70        )
71    }
72}
73
74/// Failure parsing a scheme marker from a wire string.
75#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
76#[error("expected '{expected}', got '{got}'")]
77pub struct SchemeMarkerError {
78    /// Canonical wire value.
79    pub expected: &'static str,
80    /// Observed value.
81    pub got: String,
82}
83
84/// Unit marker for the string `"exact"`.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86pub struct ExactScheme;
87
88/// Unit marker for the string `"upto"`.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90pub struct UptoScheme;
91
92/// Unit marker for the string `"batch-settlement"`.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94pub struct BatchSettlementScheme;
95
96/// Unit marker for the string `"auth-capture"`.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98pub struct AuthCaptureScheme;
99
100macro_rules! impl_scheme_marker {
101    ($ty:ty, $value:literal) => {
102        impl $ty {
103            /// Canonical wire value.
104            pub const VALUE: &'static str = $value;
105        }
106
107        impl std::fmt::Display for $ty {
108            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109                f.write_str(Self::VALUE)
110            }
111        }
112
113        impl AsRef<str> for $ty {
114            fn as_ref(&self) -> &str {
115                Self::VALUE
116            }
117        }
118
119        impl std::str::FromStr for $ty {
120            type Err = SchemeMarkerError;
121            fn from_str(s: &str) -> Result<Self, Self::Err> {
122                if s == Self::VALUE {
123                    Ok(Self)
124                } else {
125                    Err(SchemeMarkerError {
126                        expected: Self::VALUE,
127                        got: s.to_owned(),
128                    })
129                }
130            }
131        }
132
133        impl serde::Serialize for $ty {
134            fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
135                serializer.serialize_str(Self::VALUE)
136            }
137        }
138
139        impl<'de> serde::Deserialize<'de> for $ty {
140            fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
141                let s = <std::borrow::Cow<'de, str>>::deserialize(deserializer)?;
142                let s = s.as_ref();
143                if s == Self::VALUE {
144                    Ok(Self)
145                } else {
146                    Err(serde::de::Error::custom(format!(
147                        "expected '{}', got '{s}'",
148                        Self::VALUE
149                    )))
150                }
151            }
152        }
153    };
154}
155
156impl_scheme_marker!(ExactScheme, "exact");
157impl_scheme_marker!(UptoScheme, "upto");
158impl_scheme_marker!(BatchSettlementScheme, "batch-settlement");
159impl_scheme_marker!(AuthCaptureScheme, "auth-capture");
160
161#[cfg(test)]
162#[allow(clippy::unwrap_used, reason = "unit tests panic on assertion failure")]
163mod marker_tests {
164    use super::*;
165
166    #[test]
167    fn exact_serde_roundtrip() {
168        let encoded = serde_json::to_string(&ExactScheme).unwrap();
169        assert_eq!(encoded, r#""exact""#);
170        let decoded: ExactScheme = serde_json::from_str(&encoded).unwrap();
171        assert_eq!(decoded, ExactScheme);
172    }
173
174    #[test]
175    fn upto_serde_roundtrip() {
176        let encoded = serde_json::to_string(&UptoScheme).unwrap();
177        assert_eq!(encoded, r#""upto""#);
178        let decoded: UptoScheme = serde_json::from_str(&encoded).unwrap();
179        assert_eq!(decoded, UptoScheme);
180    }
181
182    #[test]
183    fn batch_settlement_serde_roundtrip() {
184        let encoded = serde_json::to_string(&BatchSettlementScheme).unwrap();
185        assert_eq!(encoded, r#""batch-settlement""#);
186        let decoded: BatchSettlementScheme = serde_json::from_str(&encoded).unwrap();
187        assert_eq!(decoded, BatchSettlementScheme);
188    }
189
190    #[test]
191    fn auth_capture_serde_roundtrip() {
192        let encoded = serde_json::to_string(&AuthCaptureScheme).unwrap();
193        assert_eq!(encoded, r#""auth-capture""#);
194        let decoded: AuthCaptureScheme = serde_json::from_str(&encoded).unwrap();
195        assert_eq!(decoded, AuthCaptureScheme);
196    }
197
198    #[test]
199    fn wrong_scheme_rejected() {
200        assert!(serde_json::from_str::<ExactScheme>(r#""upto""#).is_err());
201        assert!(serde_json::from_str::<UptoScheme>(r#""exact""#).is_err());
202        assert!(serde_json::from_str::<BatchSettlementScheme>(r#""exact""#).is_err());
203        assert!(serde_json::from_str::<AuthCaptureScheme>(r#""batch-settlement""#).is_err());
204    }
205
206    #[test]
207    fn markers_decode_from_owned_value() {
208        let exact: ExactScheme = serde_json::from_value(serde_json::json!("exact")).unwrap();
209        assert_eq!(exact, ExactScheme);
210        let upto: UptoScheme = serde_json::from_value(serde_json::json!("upto")).unwrap();
211        assert_eq!(upto, UptoScheme);
212        let batch: BatchSettlementScheme =
213            serde_json::from_value(serde_json::json!("batch-settlement")).unwrap();
214        assert_eq!(batch, BatchSettlementScheme);
215        let auth: AuthCaptureScheme =
216            serde_json::from_value(serde_json::json!("auth-capture")).unwrap();
217        assert_eq!(auth, AuthCaptureScheme);
218        assert!(serde_json::from_value::<ExactScheme>(serde_json::json!("upto")).is_err());
219    }
220
221    #[test]
222    fn slug_display_and_wildcard() {
223        let slug = SchemeSlug::new(ChainId::new("eip155", "8453"), "exact".into());
224        assert_eq!(slug.to_string(), "eip155:8453:exact");
225        let wild = slug.as_wildcard();
226        assert!(wild.is_wildcard());
227        assert_eq!(wild.to_string(), "eip155:*:exact");
228    }
229}