1use std::sync::Arc;
8
9use canton_core::auth::{self, Intercepted};
10use canton_core::telemetry::{self, TRANSPORT_GRPC};
11use canton_core::{Config, 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)]
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
70impl TopologyClient {
71 pub fn connect_lazy(config: Config) -> Result<Self> {
78 Ok(Self {
79 channel: config.connect_channel()?,
80 config: Arc::new(config),
81 })
82 }
83
84 async fn intercepted(&self) -> Result<Intercepted> {
85 auth::intercepted(&self.channel, self.config.auth()).await
86 }
87
88 async fn with_retry<T, F, Fut>(&self, op: F) -> Result<T>
89 where
90 F: FnMut() -> Fut,
91 Fut: std::future::Future<Output = Result<T>>,
92 {
93 canton_core::retry::run_with_retry(self.config.retry(), op).await
94 }
95
96 fn base_query(store: &Store) -> topo::BaseQuery {
98 topo::BaseQuery {
99 store: Some(store.to_store_id()),
100 proposals: false,
101 operation: 0, time_query: Some(topo::base_query::TimeQuery::HeadState(())),
103 filter_signed_key: String::new(),
104 protocol_version: None,
105 }
106 }
107
108 pub async fn list_party_to_participant(
114 &self,
115 store: Store,
116 filter_party: &str,
117 filter_participant: &str,
118 ) -> Result<Vec<Entry<protocol::PartyToParticipant>>> {
119 let filter_party = filter_party.to_string();
120 let filter_participant = filter_participant.to_string();
121 telemetry::instrument("list_party_to_participant", TRANSPORT_GRPC, async move {
122 self.with_retry(|| {
123 let (store, filter_party, filter_participant) = (
124 store.clone(),
125 filter_party.clone(),
126 filter_participant.clone(),
127 );
128 async move {
129 let mut client =
130 TopologyManagerReadServiceClient::new(self.intercepted().await?);
131 let response = client
132 .list_party_to_participant(topo::ListPartyToParticipantRequest {
133 base_query: Some(Self::base_query(&store)),
134 filter_party,
135 filter_participant,
136 })
137 .await?
138 .into_inner();
139 Ok(collect_entries(
140 response.results.into_iter().map(|r| (r.context, r.item)),
141 ))
142 }
143 })
144 .await
145 })
146 .await
147 }
148
149 pub async fn list_namespace_delegations(
155 &self,
156 store: Store,
157 filter_namespace: &str,
158 filter_target_key_fingerprint: &str,
159 ) -> Result<Vec<Entry<protocol::NamespaceDelegation>>> {
160 let filter_namespace = filter_namespace.to_string();
161 let filter_target_key_fingerprint = filter_target_key_fingerprint.to_string();
162 telemetry::instrument("list_namespace_delegations", TRANSPORT_GRPC, async move {
163 self.with_retry(|| {
164 let (store, filter_namespace, filter_target_key_fingerprint) = (
165 store.clone(),
166 filter_namespace.clone(),
167 filter_target_key_fingerprint.clone(),
168 );
169 async move {
170 let mut client =
171 TopologyManagerReadServiceClient::new(self.intercepted().await?);
172 let response = client
173 .list_namespace_delegation(topo::ListNamespaceDelegationRequest {
174 base_query: Some(Self::base_query(&store)),
175 filter_namespace,
176 filter_target_key_fingerprint,
177 })
178 .await?
179 .into_inner();
180 Ok(collect_entries(
181 response.results.into_iter().map(|r| (r.context, r.item)),
182 ))
183 }
184 })
185 .await
186 })
187 .await
188 }
189
190 pub async fn list_vetted_packages(
200 &self,
201 store: Store,
202 filter_participant: &str,
203 ) -> Result<Vec<Entry<protocol::VettedPackages>>> {
204 let filter_participant = filter_participant.to_string();
205 telemetry::instrument("list_vetted_packages", TRANSPORT_GRPC, async move {
206 self.with_retry(|| {
207 let (store, filter_participant) = (store.clone(), filter_participant.clone());
208 async move {
209 let mut client =
210 TopologyManagerReadServiceClient::new(self.intercepted().await?);
211 let response = client
212 .list_vetted_packages(topo::ListVettedPackagesRequest {
213 base_query: Some(Self::base_query(&store)),
214 filter_participant,
215 })
216 .await?
217 .into_inner();
218 Ok(collect_entries(
219 response.results.into_iter().map(|r| (r.context, r.item)),
220 ))
221 }
222 })
223 .await
224 })
225 .await
226 }
227}
228
229fn collect_entries<T>(
231 results: impl Iterator<Item = (Option<topo::BaseResult>, Option<T>)>,
232) -> Vec<Entry<T>> {
233 results
234 .filter_map(|(context, item)| {
235 Some(Entry {
236 context: context?,
237 item: item?,
238 })
239 })
240 .collect()
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn store_maps_to_the_right_store_id_variant() {
249 use topo::store_id::Store as S;
250
251 assert!(matches!(
252 Store::Authorized.to_store_id().store,
253 Some(S::Authorized(_))
254 ));
255
256 match Store::Synchronizer("pid-1".to_string()).to_store_id().store {
257 Some(S::Synchronizer(s)) => assert_eq!(
258 s.kind,
259 Some(topo::synchronizer::Kind::PhysicalId("pid-1".to_string()))
260 ),
261 other => panic!("expected a synchronizer store, got {other:?}"),
262 }
263
264 match Store::Temporary("scratch".to_string()).to_store_id().store {
265 Some(S::Temporary(t)) => assert_eq!(t.name, "scratch"),
266 other => panic!("expected a temporary store, got {other:?}"),
267 }
268 }
269
270 #[test]
271 fn base_query_asks_for_head_state_without_proposals() {
272 let query = TopologyClient::base_query(&Store::Authorized);
273 assert!(matches!(
274 query.time_query,
275 Some(topo::base_query::TimeQuery::HeadState(()))
276 ));
277 assert!(!query.proposals, "reads default to committed state");
278 assert!(query.store.is_some());
279 }
280
281 #[test]
282 fn collect_entries_drops_partial_results() {
283 let ctx = topo::BaseResult::default();
284 let rows = vec![
285 (Some(ctx.clone()), Some(7u8)),
286 (None, Some(8u8)), (Some(ctx.clone()), None), ];
289 let kept = collect_entries(rows.into_iter());
290 assert_eq!(kept.len(), 1);
291 assert_eq!(kept[0].item, 7);
292 }
293}