r402_core/wire/
payment_requirements.rs1use std::str::FromStr;
4
5use compact_str::CompactString;
6use serde::de::DeserializeOwned;
7use serde::{Deserialize, Serialize};
8
9use crate::chain::ChainId;
10
11#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase", deny_unknown_fields)]
19#[non_exhaustive]
20pub struct PaymentRequirements<
21 TScheme = CompactString,
22 TAmount = CompactString,
23 TAddress = CompactString,
24 TExtra = serde_json::Value,
25> {
26 pub scheme: TScheme,
28 pub network: ChainId,
30 pub amount: TAmount,
32 pub pay_to: TAddress,
34 pub max_timeout_seconds: u64,
36 pub asset: TAddress,
38 #[serde(default = "Option::default", skip_serializing_if = "Option::is_none")]
40 pub extra: Option<TExtra>,
41}
42
43#[must_use]
48pub fn find_matching_requirements<'a>(
49 available: &'a [PaymentRequirements],
50 accepted: &PaymentRequirements,
51) -> Option<&'a PaymentRequirements> {
52 available
53 .iter()
54 .find(|req| req.matches_payload_accepted(accepted))
55}
56
57impl<TScheme, TAmount, TAddress, TExtra> PaymentRequirements<TScheme, TAmount, TAddress, TExtra> {
58 #[must_use]
62 pub const fn new(
63 scheme: TScheme,
64 network: ChainId,
65 amount: TAmount,
66 pay_to: TAddress,
67 asset: TAddress,
68 max_timeout_seconds: u64,
69 ) -> Self {
70 Self {
71 scheme,
72 network,
73 amount,
74 pay_to,
75 asset,
76 max_timeout_seconds,
77 extra: None,
78 }
79 }
80
81 #[must_use]
83 pub fn with_extra(mut self, extra: TExtra) -> Self {
84 self.extra = Some(extra);
85 self
86 }
87
88 #[must_use]
91 pub fn with_optional_extra(mut self, extra: Option<TExtra>) -> Self {
92 self.extra = extra;
93 self
94 }
95}
96
97impl PaymentRequirements {
98 #[must_use]
105 pub fn matches_payload_accepted(&self, accepted: &Self) -> bool {
106 self.scheme == accepted.scheme
107 && self.network == accepted.network
108 && self.amount == accepted.amount
109 && self.asset == accepted.asset
110 && self.pay_to == accepted.pay_to
111 }
112
113 #[must_use]
118 pub fn as_concrete<TScheme, TAmount, TAddress, TExtra>(
119 &self,
120 ) -> Option<PaymentRequirements<TScheme, TAmount, TAddress, TExtra>>
121 where
122 TScheme: FromStr,
123 TAmount: FromStr,
124 TAddress: FromStr,
125 TExtra: DeserializeOwned,
126 {
127 let scheme = self.scheme.parse::<TScheme>().ok()?;
128 let amount = self.amount.parse::<TAmount>().ok()?;
129 let pay_to = self.pay_to.parse::<TAddress>().ok()?;
130 let asset = self.asset.parse::<TAddress>().ok()?;
131 let extra = self
132 .extra
133 .as_ref()
134 .and_then(|v| serde_json::from_value(v.clone()).ok());
135 Some(PaymentRequirements {
136 scheme,
137 network: self.network.clone(),
138 amount,
139 pay_to,
140 max_timeout_seconds: self.max_timeout_seconds,
141 asset,
142 extra,
143 })
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
154 fn rejects_unknown_top_level_field() {
155 let json = serde_json::json!({
156 "scheme": "exact",
157 "network": "eip155:8453",
158 "amount": "1",
159 "payTo": "0x0",
160 "maxTimeoutSeconds": 60,
161 "asset": "0x0",
162 "unknownField": 1
163 });
164 assert!(serde_json::from_value::<PaymentRequirements>(json).is_err());
165 }
166
167 #[test]
169 fn find_matching_requirements_go_semantics() {
170 let a = PaymentRequirements::new(
171 "exact".into(),
172 "eip155:1".parse().unwrap(),
173 "1000000".into(),
174 "0xrecipient1".into(),
175 "USDC".into(),
176 60,
177 );
178 let b = PaymentRequirements::new(
179 "exact".into(),
180 "eip155:8453".parse().unwrap(),
181 "2000000".into(),
182 "0xrecipient2".into(),
183 "USDC".into(),
184 30,
185 );
186 let available = [a.clone(), b.clone()];
187
188 let mut accepted = b;
190 accepted.max_timeout_seconds = 999;
191 let matched = find_matching_requirements(&available, &accepted).unwrap();
192 assert_eq!(matched.network.to_string(), "eip155:8453");
193 assert_eq!(matched.max_timeout_seconds, 30); let mut miss = a;
197 miss.scheme = "nonexistent".into();
198 assert!(find_matching_requirements(&available, &miss).is_none());
199 }
200}