Skip to main content

server_rpc/
error.rs

1
2use std::borrow::Borrow;
3use std::str::FromStr;
4
5use bitcoin::hex::FromHex;
6use log::debug;
7
8use ark::VtxoId;
9
10/// Extension trait for [tonic::Status]
11pub trait StatusExt: Borrow<tonic::Status> {
12	/// Whether the server rejected the request and no state has changed
13	fn is_rejection(&self) -> bool;
14
15	/// Get any not-found identifiers
16	fn not_found<T: FromHex>(&self) -> Option<Vec<T>>;
17
18	/// The VTXO ids the server flagged as the reason it rejected the request.
19	///
20	/// Returns an empty vec when the status is not a rejection, carries no
21	/// identifiers (e.g. an older server), or none of them parse as a vtxo id.
22	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}