1
2use std::borrow::Borrow;
3use std::str::FromStr;
4
5use bitcoin::hex::FromHex;
6use log::debug;
7
8use ark::VtxoId;
9
10pub trait StatusExt: Borrow<tonic::Status> {
12 fn is_rejection(&self) -> bool;
14
15 fn not_found<T: FromHex>(&self) -> Option<Vec<T>>;
17
18 fn rejected_vtxos(&self) -> Vec<VtxoId>;
23}
24
25impl StatusExt for tonic::Status {
26 fn is_rejection(&self) -> bool {
27 match self.code() {
28 tonic::Code::InvalidArgument | tonic::Code::NotFound => true,
29 _ => false,
30 }
31 }
32
33 fn not_found<T: FromHex>(&self) -> Option<Vec<T>> {
34 if self.code() != tonic::Code::NotFound {
35 return None;
36 }
37
38 let ids = match self.metadata().get("identifiers") {
39 Some(v) => v,
40 None => {
41 debug!("Server sent NOT_FOUND error without identifiers");
42 return Some(vec![]);
43 },
44 };
45 let mut ret = Vec::new();
46 let ids_str = match ids.to_str() {
47 Ok(v) => v,
48 Err(e) => {
49 debug!("Invalid (non-ASCII) value in NOT_FOUND identifiers metadata: {:#}", e);
50 return Some(vec![]);
51 },
52 };
53 for value in ids_str.split(',') {
54 match T::from_hex(value) {
55 Ok(v) => ret.push(v),
56 Err(e) => {
57 debug!("Server NOT_FOUND identifier could not be parsed: {:#}", e);
58 },
59 }
60 }
61 Some(ret)
62 }
63
64 fn rejected_vtxos(&self) -> Vec<VtxoId> {
65 if !self.is_rejection() {
66 return vec![];
67 }
68
69 let ids = match self.metadata().get("identifiers") {
70 Some(v) => v,
71 None => return vec![],
72 };
73 let ids_str = match ids.to_str() {
74 Ok(v) => v,
75 Err(e) => {
76 debug!("Invalid (non-ASCII) value in identifiers metadata: {:#}", e);
77 return vec![];
78 },
79 };
80
81 ids_str.split(',')
82 .filter(|s| !s.is_empty())
83 .filter_map(|s| match VtxoId::from_str(s) {
84 Ok(id) => Some(id),
85 Err(e) => {
86 debug!("Server identifier could not be parsed as a vtxo id: {:#}", e);
87 None
88 },
89 })
90 .collect()
91 }
92}
93
94#[cfg(test)]
95mod test {
96 use super::*;
97
98 const VTXO_A: &str = "0000000000000000000000000000000000000000000000000000000000000001:0";
99 const VTXO_B: &str = "0000000000000000000000000000000000000000000000000000000000000002:1";
100
101 #[test]
102 fn rejected_vtxos_parses_identifiers_metadata() {
103 let a = VtxoId::from_str(VTXO_A).unwrap();
104 let b = VtxoId::from_str(VTXO_B).unwrap();
105 let mut status = tonic::Status::invalid_argument("input vtxo(s) not spendable");
106 status.metadata_mut().insert("identifiers", format!("{},{}", a, b).parse().unwrap());
107 assert_eq!(status.rejected_vtxos(), vec![a, b]);
108 }
109
110 #[test]
111 fn rejected_vtxos_empty_without_metadata() {
112 assert!(tonic::Status::invalid_argument("nope").rejected_vtxos().is_empty());
113 }
114
115 #[test]
116 fn rejected_vtxos_ignores_non_rejection_codes() {
117 let a = VtxoId::from_str(VTXO_A).unwrap();
118 let mut status = tonic::Status::internal("boom");
119 status.metadata_mut().insert("identifiers", a.to_string().parse().unwrap());
120 assert!(status.rejected_vtxos().is_empty());
121 }
122
123 #[test]
124 fn rejected_vtxos_skips_unparseable_entries() {
125 let a = VtxoId::from_str(VTXO_A).unwrap();
126 let mut status = tonic::Status::invalid_argument("nope");
127 status.metadata_mut().insert("identifiers", format!("garbage,{}", a).parse().unwrap());
128 assert_eq!(status.rejected_vtxos(), vec![a]);
129 }
130}