1use bitcoin::{
2 Address, Amount, Denomination, Network,
3 address::{NetworkChecked, NetworkUnchecked, NetworkValidation},
4};
5use std::borrow::Cow;
6use std::collections::{HashMap, HashSet};
7use urlencoding::{decode, encode};
8
9#[derive(Debug, PartialEq, Eq, Clone)]
10pub enum Bip321Errors<'a> {
11 DuplicateParam(&'a str),
12 IncorrectSchema,
13 InvalidAddress(&'a str),
14 InvalidAmount,
15 NoOnePaymentWasFound,
16 InvalidEncoding,
17 InvalidRequiredPayment,
18}
19
20pub trait Bip321ExtraHandle<'a>
21where
22 Self: Default,
23{
24 fn handle_param(
25 &mut self,
26 key: &'a str,
27 value: Vec<Cow<'a, str>>,
28 ) -> Result<(), Bip321Errors<'a>>;
29
30 fn validate(&self, _network: Network) -> Result<(), Bip321Errors<'a>> {
31 Ok(())
32 }
33
34 fn is_empty(&self) -> bool;
35
36 fn is_supported_key(&self, key: &str) -> bool;
37}
38
39#[derive(Debug, PartialEq, Eq, Clone)]
40pub struct PopConfig<'a> {
41 pop: Cow<'a, str>,
42 pub required: bool,
43}
44
45impl<'a> PopConfig<'a> {
46 pub fn finalize_uri(
47 &self,
48 source_key: &str,
49 payment_data_hex: &str,
50 ) -> Result<String, Bip321Errors<'a>> {
51 if !payment_data_hex.chars().all(|c| c.is_ascii_hexdigit()) {
52 return Err(Bip321Errors::InvalidEncoding);
53 };
54
55 let append_to_pop = format!("{}{}={}", self.pop, source_key, payment_data_hex);
56
57 Ok(append_to_pop)
58 }
59}
60
61#[derive(Debug, PartialEq, Eq, Clone)]
62pub struct Bip321<'a, T, NetVal = NetworkUnchecked>
63where
64 T: Bip321ExtraHandle<'a>,
65 NetVal: NetworkValidation,
66{
67 pub address: Option<Address<NetVal>>,
68 pub amount: Option<Amount>,
69 pub label: Option<Cow<'a, str>>,
70 pub message: Option<Cow<'a, str>>,
71 pub pop: Option<PopConfig<'a>>,
72 pub extras: Option<T>,
73}
74
75impl<'a, T: Bip321ExtraHandle<'a>> Bip321<'a, T> {
76 pub fn parse_url(s: &'a str) -> Result<Self, Bip321Errors<'a>> {
77 let uri = s.trim();
78
79 if uri.len() < 8 || !uri[..8].eq_ignore_ascii_case("bitcoin:") {
80 return Err(Bip321Errors::IncorrectSchema);
81 }
82
83 let body = &uri[8..];
84 let (address_str, query_str) = match body.find("?") {
85 Some(pos) => (&body[..pos], &body[pos + 1..]),
86 None => (&body[..], ""),
87 };
88
89 let mut seens: HashSet<&'a str> = HashSet::new();
90 let mut extra_params: HashMap<&'a str, Vec<Cow<'a, str>>> = HashMap::new();
91 let mut result: Bip321<T, NetworkUnchecked> = Bip321::default();
92
93 if !address_str.is_empty() {
94 let addr = address_str
95 .parse::<Address<NetworkUnchecked>>()
96 .map_err(|_| Bip321Errors::InvalidAddress(address_str))?;
97
98 result.address = Some(addr);
99 }
100
101 if !query_str.is_empty() {
102 for param in query_str.split("&") {
103 let (key, value) = param.split_once("=").unwrap_or((param, ""));
104
105 let is_pop_related =
106 key.eq_ignore_ascii_case("pop") || key.eq_ignore_ascii_case("req-pop");
107
108 if is_pop_related {
109 if !seens.insert("pop") {
110 return Err(Bip321Errors::DuplicateParam(key));
111 }
112 } else if matches!(key, "amount" | "label" | "message") {
113 if !seens.insert(key) {
114 return Err(Bip321Errors::DuplicateParam(key));
115 }
116 }
117
118 match key {
119 "amount" => {
120 if value.contains(",") {
121 return Err(Bip321Errors::InvalidAmount);
122 }
123 let amt = Amount::from_str_in(value, Denomination::Bitcoin)
124 .map_err(|_| Bip321Errors::InvalidAmount)?;
125 result.amount = Some(amt);
126 }
127 "label" => {
128 result.label =
129 Some(decode(value).map_err(|_| Bip321Errors::InvalidEncoding)?);
130 }
131 "message" => {
132 result.message =
133 Some(decode(value).map_err(|_| Bip321Errors::InvalidEncoding)?);
134 }
135 "pop" | "req-pop" => {
136 let forbidden_schemes = ["http", "https", "file", "javascript", "mailto"];
137 let decoded_val = decode(value)
138 .map(|s| s)
139 .map_err(|_| Bip321Errors::InvalidEncoding)?;
140 let value_lower = decoded_val.to_lowercase();
141
142 if forbidden_schemes
143 .iter()
144 .any(|&s| value_lower.starts_with(s))
145 {
146 return Err(Bip321Errors::IncorrectSchema);
147 } else {
148 result.pop = if key != "pop" {
149 Some(PopConfig {
150 pop: decoded_val,
151 required: true,
152 })
153 } else {
154 Some(PopConfig {
155 pop: decoded_val,
156 required: false,
157 })
158 };
159 }
160 }
161 _ => {
162 let decoded_val = decode(value)
163 .map(|s| s)
164 .map_err(|_| Bip321Errors::InvalidEncoding)?;
165 extra_params
166 .entry(key)
167 .or_insert(Vec::new())
168 .push(decoded_val);
169 }
170 }
171 }
172 }
173
174 for (key, values) in extra_params {
175 let ext = result.extras.get_or_insert_with(T::default);
176 if key.starts_with("req-") {
177 let stripped = &key[4..];
178 if !ext.is_supported_key(stripped) {
179 return Err(Bip321Errors::InvalidRequiredPayment);
180 }
181 ext.handle_param(stripped, values)?;
182 } else {
183 ext.handle_param(key, values)?
184 }
185 }
186
187 if let Some(ext) = result.extras.as_ref() {
188 if ext.is_empty() {
189 result.extras = None;
190 }
191 }
192
193 if result.address.is_none() && result.extras.is_none() {
194 return Err(Bip321Errors::NoOnePaymentWasFound);
195 }
196
197 Ok(result)
198 }
199
200 pub fn build(&self) -> String {
201 let mut uri = String::from("bitcoin:");
202
203 if let Some(ref addr) = self.address {
204 let address = addr.clone().assume_checked().to_string();
205 uri.push_str(&format!("{}", address));
206 }
207
208 let mut params: Vec<String> = Vec::new();
209
210 if let Some(amount) = self.amount {
211 params.push(format!("amount={}", amount.to_btc()));
212 }
213
214 if let Some(label) = &self.label {
215 params.push(format!("label={}", encode(label)));
216 }
217
218 if let Some(ref message) = self.message {
219 params.push(format!("message={}", encode(message)));
220 }
221
222 if let Some(ref pop_conf) = self.pop {
223 if pop_conf.required {
224 params.push(format!("req-pop={}", encode(&pop_conf.pop)));
225 } else {
226 params.push(format!("pop={}", encode(&pop_conf.pop)));
227 }
228 }
229
230 if !params.is_empty() {
231 uri.push('?');
232 uri.push_str(¶ms.join("&"));
233 }
234
235 uri
236 }
237}
238impl<'a, T: Bip321ExtraHandle<'a>> Bip321<'a, T, NetworkUnchecked> {
239 pub fn into_checked(
240 self,
241 network: Network,
242 ) -> Result<Bip321<'a, T, NetworkChecked>, Bip321Errors<'a>> {
243 let checked_addr = match self.address {
244 Some(addr) => {
245 let checked = addr
246 .require_network(network)
247 .map_err(|_| Bip321Errors::InvalidAddress("Wrong Network"))?;
248 Some(checked)
249 }
250 None => None,
251 };
252
253 if let Some(ext) = self.extras.as_ref() {
254 ext.validate(network)?;
255 }
256
257 Ok(Bip321 {
258 address: checked_addr,
259 amount: self.amount,
260 label: self.label,
261 message: self.message,
262 pop: self.pop,
263 extras: self.extras,
264 })
265 }
266}
267
268impl<'a, T: Bip321ExtraHandle<'a>> Default for Bip321<'a, T, NetworkUnchecked> {
269 fn default() -> Self {
270 Bip321 {
271 address: None,
272 amount: None,
273 label: None,
274 message: None,
275 pop: None,
276 extras: None,
277 }
278 }
279}
280
281#[derive(Debug, PartialEq, Eq, Clone, Default)]
282pub struct ExtraExample {
283 pub pj: Vec<String>,
284 pub sp: Vec<String>,
285 pub lightning: Vec<String>,
286}
287
288impl<'a> Bip321ExtraHandle<'a> for ExtraExample {
289 fn handle_param(
290 &mut self,
291 key: &'a str,
292 values: Vec<Cow<'a, str>>,
293 ) -> Result<(), Bip321Errors<'a>> {
294 match key {
295 "pj" => {
296 for val in values {
297 self.pj.push(val.to_string());
298 }
299 Ok(())
300 }
301 "lightning" => {
302 for val in values {
303 self.lightning.push(val.to_string());
304 }
305 Ok(())
306 }
307 "sp" => {
308 for val in values {
309 self.sp.push(val.to_string());
310 }
311 Ok(())
312 }
313 _ => Ok(()),
314 }
315 }
316
317 fn is_empty(&self) -> bool {
318 self.pj.is_empty() && self.lightning.is_empty() && self.sp.is_empty()
319 }
320
321 fn is_supported_key(&self, key: &str) -> bool {
322 matches!(key, "pj" | "lightning" | "sp")
323 }
324}