1use std::sync::Arc;
8
9use canton_core::auth::{self, Intercepted};
10use canton_core::telemetry::{self, TRANSPORT_GRPC};
11use canton_core::{Config, Error, Result};
12use canton_proto::com::digitalasset::canton::protocol::v30 as protocol;
13use canton_proto::com::digitalasset::canton::topology::admin::v30 as topo;
14use tonic::transport::Channel;
15use topo::topology_manager_read_service_client::TopologyManagerReadServiceClient;
16
17#[derive(Clone, Debug, PartialEq, Eq, Hash)]
22#[non_exhaustive]
23pub enum Store {
24 Authorized,
26 Synchronizer(String),
28 Temporary(String),
30}
31
32impl Store {
33 fn to_store_id(&self) -> topo::StoreId {
34 use topo::store_id::Store as S;
35 let store = match self {
36 Store::Authorized => S::Authorized(topo::store_id::Authorized {}),
37 Store::Synchronizer(physical_id) => S::Synchronizer(topo::Synchronizer {
38 kind: Some(topo::synchronizer::Kind::PhysicalId(physical_id.clone())),
39 }),
40 Store::Temporary(name) => {
41 S::Temporary(topo::store_id::Temporary { name: name.clone() })
42 }
43 };
44 topo::StoreId { store: Some(store) }
45 }
46}
47
48#[derive(Clone, Debug, PartialEq)]
52#[non_exhaustive]
53pub struct Entry<T> {
54 pub context: topo::BaseResult,
56 pub item: T,
58}
59
60#[derive(Clone, Debug)]
65pub struct TopologyClient {
66 channel: Channel,
67 config: Arc<Config>,
68}
69
70macro_rules! service {
80 ($self:ident, $ctor:expr) => {
81 $ctor($self.intercepted().await?)
82 .max_decoding_message_size($self.config.max_decoding_message_size())
83 };
84}
85
86impl TopologyClient {
87 pub fn connect_lazy(config: Config) -> Result<Self> {
94 Ok(Self {
95 channel: config.connect_channel()?,
96 config: Arc::new(config),
97 })
98 }
99
100 async fn intercepted(&self) -> Result<Intercepted> {
101 auth::intercepted(&self.channel, self.config.auth()).await
102 }
103
104 async fn with_retry<T, F, Fut>(&self, op: F) -> Result<T>
105 where
106 F: FnMut() -> Fut,
107 Fut: std::future::Future<Output = Result<T>>,
108 {
109 canton_core::retry::run_with_retry(self.config.retry(), op).await
110 }
111
112 fn base_query(store: &Store) -> topo::BaseQuery {
114 topo::BaseQuery {
115 store: Some(store.to_store_id()),
116 proposals: false,
117 operation: 0, time_query: Some(topo::base_query::TimeQuery::HeadState(())),
119 filter_signed_key: String::new(),
120 protocol_version: None,
121 }
122 }
123
124 pub async fn list_party_to_participant(
130 &self,
131 store: Store,
132 filter_party: &str,
133 filter_participant: &str,
134 ) -> Result<Vec<Entry<protocol::PartyToParticipant>>> {
135 let filter_party = filter_party.to_string();
136 let filter_participant = filter_participant.to_string();
137 telemetry::instrument("list_party_to_participant", TRANSPORT_GRPC, async move {
138 self.with_retry(|| {
139 let (store, filter_party, filter_participant) = (
140 store.clone(),
141 filter_party.clone(),
142 filter_participant.clone(),
143 );
144 async move {
145 let mut client = service!(self, TopologyManagerReadServiceClient::new);
146 let response = client
147 .list_party_to_participant(topo::ListPartyToParticipantRequest {
148 base_query: Some(Self::base_query(&store)),
149 filter_party,
150 filter_participant,
151 })
152 .await?
153 .into_inner();
154 collect_entries(response.results.into_iter().map(|r| (r.context, r.item)))
155 }
156 })
157 .await
158 })
159 .await
160 }
161
162 pub async fn list_namespace_delegations(
168 &self,
169 store: Store,
170 filter_namespace: &str,
171 filter_target_key_fingerprint: &str,
172 ) -> Result<Vec<Entry<protocol::NamespaceDelegation>>> {
173 let filter_namespace = filter_namespace.to_string();
174 let filter_target_key_fingerprint = filter_target_key_fingerprint.to_string();
175 telemetry::instrument("list_namespace_delegations", TRANSPORT_GRPC, async move {
176 self.with_retry(|| {
177 let (store, filter_namespace, filter_target_key_fingerprint) = (
178 store.clone(),
179 filter_namespace.clone(),
180 filter_target_key_fingerprint.clone(),
181 );
182 async move {
183 let mut client = service!(self, TopologyManagerReadServiceClient::new);
184 let response = client
185 .list_namespace_delegation(topo::ListNamespaceDelegationRequest {
186 base_query: Some(Self::base_query(&store)),
187 filter_namespace,
188 filter_target_key_fingerprint,
189 })
190 .await?
191 .into_inner();
192 collect_entries(response.results.into_iter().map(|r| (r.context, r.item)))
193 }
194 })
195 .await
196 })
197 .await
198 }
199
200 pub async fn list_vetted_packages(
210 &self,
211 store: Store,
212 filter_participant: &str,
213 ) -> Result<Vec<Entry<protocol::VettedPackages>>> {
214 let filter_participant = filter_participant.to_string();
215 telemetry::instrument("list_vetted_packages", TRANSPORT_GRPC, async move {
216 self.with_retry(|| {
217 let (store, filter_participant) = (store.clone(), filter_participant.clone());
218 async move {
219 let mut client = service!(self, TopologyManagerReadServiceClient::new);
220 let response = client
221 .list_vetted_packages(topo::ListVettedPackagesRequest {
222 base_query: Some(Self::base_query(&store)),
223 filter_participant,
224 })
225 .await?
226 .into_inner();
227 collect_entries(response.results.into_iter().map(|r| (r.context, r.item)))
228 }
229 })
230 .await
231 })
232 .await
233 }
234}
235
236fn collect_entries<T>(
246 results: impl Iterator<Item = (Option<topo::BaseResult>, Option<T>)>,
247) -> Result<Vec<Entry<T>>> {
248 results
249 .map(|(context, item)| match (context, item) {
250 (Some(context), Some(item)) => Ok(Entry { context, item }),
251 (None, _) => Err(Error::UnexpectedResponse(
252 "a topology result carried no context; the response is incomplete".to_string(),
253 )),
254 (_, None) => Err(Error::UnexpectedResponse(
255 "a topology result carried no mapping; the response is incomplete".to_string(),
256 )),
257 })
258 .collect()
259}
260
261#[cfg(test)]
262#[allow(clippy::expect_used)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn store_maps_to_the_right_store_id_variant() {
268 use topo::store_id::Store as S;
269
270 assert!(matches!(
271 Store::Authorized.to_store_id().store,
272 Some(S::Authorized(_))
273 ));
274
275 match Store::Synchronizer("pid-1".to_string()).to_store_id().store {
276 Some(S::Synchronizer(s)) => assert_eq!(
277 s.kind,
278 Some(topo::synchronizer::Kind::PhysicalId("pid-1".to_string()))
279 ),
280 other => panic!("expected a synchronizer store, got {other:?}"),
281 }
282
283 match Store::Temporary("scratch".to_string()).to_store_id().store {
284 Some(S::Temporary(t)) => assert_eq!(t.name, "scratch"),
285 other => panic!("expected a temporary store, got {other:?}"),
286 }
287 }
288
289 #[test]
290 fn base_query_asks_for_head_state_without_proposals() {
291 let query = TopologyClient::base_query(&Store::Authorized);
292 assert!(matches!(
293 query.time_query,
294 Some(topo::base_query::TimeQuery::HeadState(()))
295 ));
296 assert!(!query.proposals, "reads default to committed state");
297 assert!(query.store.is_some());
298 }
299
300 #[test]
301 fn collect_entries_refuses_partial_results() {
302 let ctx = topo::BaseResult::default();
303
304 let rows = vec![
306 (Some(ctx.clone()), Some(7u8)),
307 (Some(ctx.clone()), Some(8u8)),
308 ];
309 let kept = collect_entries(rows.into_iter()).expect("a complete response");
310 assert_eq!(kept.len(), 2);
311
312 let missing_context = vec![(Some(ctx.clone()), Some(7u8)), (None, Some(8u8))];
315 assert!(matches!(
316 collect_entries(missing_context.into_iter()),
317 Err(Error::UnexpectedResponse(_))
318 ));
319 let missing_item = vec![(Some(ctx.clone()), Some(7u8)), (Some(ctx), None::<u8>)];
320 assert!(matches!(
321 collect_entries(missing_item.into_iter()),
322 Err(Error::UnexpectedResponse(_))
323 ));
324 }
325}