tap_msg/
settlement_address.rs1use serde::{de, Deserialize, Deserializer, Serialize};
8use std::fmt;
9use std::str::FromStr;
10use thiserror::Error;
11
12#[derive(Debug, Error)]
14pub enum SettlementAddressError {
15 #[error("Invalid PayTo URI format: {0}")]
17 InvalidPayToUri(String),
18
19 #[error("Invalid CAIP-10 format: {0}")]
21 InvalidCaip10(String),
22
23 #[error("Unknown settlement address format")]
25 UnknownFormat,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
38#[serde(transparent)]
39pub struct PayToUri(String);
40
41impl<'de> Deserialize<'de> for PayToUri {
42 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
43 where
44 D: Deserializer<'de>,
45 {
46 let s = String::deserialize(deserializer)?;
47 PayToUri::new(s).map_err(de::Error::custom)
48 }
49}
50
51impl PayToUri {
52 pub fn new(uri: String) -> Result<Self, SettlementAddressError> {
54 if !uri.starts_with("payto://") {
55 return Err(SettlementAddressError::InvalidPayToUri(
56 "PayTo URI must start with 'payto://'".to_string(),
57 ));
58 }
59
60 let after_scheme = &uri[8..]; if !after_scheme.contains('/') {
63 return Err(SettlementAddressError::InvalidPayToUri(
64 "PayTo URI must have method and account parts".to_string(),
65 ));
66 }
67
68 let parts: Vec<&str> = after_scheme.splitn(2, '/').collect();
69 if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
70 return Err(SettlementAddressError::InvalidPayToUri(
71 "PayTo URI must have non-empty method and account".to_string(),
72 ));
73 }
74
75 Ok(PayToUri(uri))
76 }
77
78 pub fn method(&self) -> &str {
80 let after_scheme = &self.0[8..];
81 after_scheme.split('/').next().unwrap_or("")
82 }
83
84 pub fn as_str(&self) -> &str {
86 &self.0
87 }
88}
89
90impl fmt::Display for PayToUri {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 write!(f, "{}", self.0)
93 }
94}
95
96impl FromStr for PayToUri {
97 type Err = SettlementAddressError;
98
99 fn from_str(s: &str) -> Result<Self, Self::Err> {
100 PayToUri::new(s.to_string())
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum SettlementAddress {
108 Caip10(String),
110
111 PayTo(PayToUri),
113}
114
115impl SettlementAddress {
116 pub fn from_string(s: String) -> Result<Self, SettlementAddressError> {
118 if s.starts_with("payto://") {
119 Ok(SettlementAddress::PayTo(PayToUri::new(s)?))
120 } else if s.contains(':') {
121 let parts: Vec<&str> = s.split(':').collect();
123 if parts.len() >= 2 && !parts[0].is_empty() && !parts[1].is_empty() {
124 Ok(SettlementAddress::Caip10(s))
125 } else {
126 Err(SettlementAddressError::InvalidCaip10(
127 "CAIP-10 must have chain_id and address parts".to_string(),
128 ))
129 }
130 } else {
131 Err(SettlementAddressError::UnknownFormat)
132 }
133 }
134
135 pub fn is_blockchain(&self) -> bool {
137 matches!(self, SettlementAddress::Caip10(_))
138 }
139
140 pub fn is_traditional(&self) -> bool {
142 matches!(self, SettlementAddress::PayTo(_))
143 }
144
145 pub fn as_str(&self) -> &str {
147 match self {
148 SettlementAddress::Caip10(s) => s,
149 SettlementAddress::PayTo(uri) => uri.as_str(),
150 }
151 }
152}
153
154impl fmt::Display for SettlementAddress {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 write!(f, "{}", self.as_str())
157 }
158}
159
160impl Serialize for SettlementAddress {
161 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
162 where
163 S: serde::Serializer,
164 {
165 serializer.serialize_str(self.as_str())
166 }
167}
168
169impl<'de> Deserialize<'de> for SettlementAddress {
170 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
171 where
172 D: serde::Deserializer<'de>,
173 {
174 let s = String::deserialize(deserializer)?;
175 SettlementAddress::from_string(s).map_err(serde::de::Error::custom)
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn test_payto_uri_creation() {
185 let uri = PayToUri::new("payto://iban/DE75512108001245126199".to_string()).unwrap();
186 assert_eq!(uri.method(), "iban");
187 assert_eq!(uri.as_str(), "payto://iban/DE75512108001245126199");
188 }
189
190 #[test]
191 fn test_payto_uri_with_parameters() {
192 let uri = PayToUri::new(
193 "payto://iban/GB33BUKB20201555555555?receiver-name=UK%20Receiver%20Ltd".to_string(),
194 )
195 .unwrap();
196 assert_eq!(uri.method(), "iban");
197 assert!(uri.as_str().contains("receiver-name"));
198 }
199
200 #[test]
201 fn test_payto_uri_various_methods() {
202 let test_cases = vec![
203 "payto://iban/DE75512108001245126199",
204 "payto://ach/122000247/111000025",
205 "payto://bic/SOGEDEFFXXX",
206 "payto://upi/9999999999@paytm",
207 ];
208
209 for case in test_cases {
210 let uri = PayToUri::new(case.to_string()).unwrap();
211 assert!(uri.as_str().starts_with("payto://"));
212 }
213 }
214
215 #[test]
216 fn test_payto_uri_invalid_format() {
217 let invalid_cases = vec![
218 "http://example.com", "payto://", "payto://iban", "payto://iban/", "iban/DE75512108001245126199", ];
224
225 for case in invalid_cases {
226 assert!(PayToUri::new(case.to_string()).is_err());
227 }
228 }
229
230 #[test]
231 fn test_settlement_address_from_payto() {
232 let addr =
233 SettlementAddress::from_string("payto://iban/DE75512108001245126199".to_string())
234 .unwrap();
235
236 assert!(addr.is_traditional());
237 assert!(!addr.is_blockchain());
238 assert_eq!(addr.as_str(), "payto://iban/DE75512108001245126199");
239 }
240
241 #[test]
242 fn test_settlement_address_from_caip10() {
243 let addr = SettlementAddress::from_string(
244 "eip155:1:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb".to_string(),
245 )
246 .unwrap();
247
248 assert!(addr.is_blockchain());
249 assert!(!addr.is_traditional());
250 assert_eq!(
251 addr.as_str(),
252 "eip155:1:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"
253 );
254 }
255
256 #[test]
257 fn test_settlement_address_simple_caip10() {
258 let addr = SettlementAddress::from_string(
260 "ethereum:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb".to_string(),
261 )
262 .unwrap();
263
264 assert!(addr.is_blockchain());
265 assert_eq!(
266 addr.as_str(),
267 "ethereum:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"
268 );
269 }
270
271 #[test]
272 fn test_settlement_address_invalid() {
273 let invalid_cases = vec![
274 "just-some-text", "", ":", "payto://", ];
279
280 for case in invalid_cases {
281 assert!(SettlementAddress::from_string(case.to_string()).is_err());
282 }
283 }
284
285 #[test]
286 fn test_settlement_address_serialization() {
287 let payto_addr =
288 SettlementAddress::from_string("payto://iban/DE75512108001245126199".to_string())
289 .unwrap();
290
291 let json = serde_json::to_string(&payto_addr).unwrap();
292 assert_eq!(json, "\"payto://iban/DE75512108001245126199\"");
293
294 let deserialized: SettlementAddress = serde_json::from_str(&json).unwrap();
295 assert_eq!(deserialized, payto_addr);
296 }
297
298 #[test]
299 fn test_settlement_address_caip10_serialization() {
300 let caip_addr = SettlementAddress::from_string(
301 "eip155:1:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb".to_string(),
302 )
303 .unwrap();
304
305 let json = serde_json::to_string(&caip_addr).unwrap();
306 assert_eq!(
307 json,
308 "\"eip155:1:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb\""
309 );
310
311 let deserialized: SettlementAddress = serde_json::from_str(&json).unwrap();
312 assert_eq!(deserialized, caip_addr);
313 }
314
315 #[test]
316 fn test_settlement_address_array_serialization() {
317 let addresses = vec![
318 SettlementAddress::from_string("payto://iban/DE75512108001245126199".to_string())
319 .unwrap(),
320 SettlementAddress::from_string(
321 "eip155:1:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb".to_string(),
322 )
323 .unwrap(),
324 ];
325
326 let json = serde_json::to_string(&addresses).unwrap();
327 assert!(json.contains("payto://iban"));
328 assert!(json.contains("eip155:1"));
329
330 let deserialized: Vec<SettlementAddress> = serde_json::from_str(&json).unwrap();
331 assert_eq!(deserialized.len(), 2);
332 assert!(deserialized[0].is_traditional());
333 assert!(deserialized[1].is_blockchain());
334 }
335}