1use std::str::FromStr;
8
9use calimero_primitives::alias::{Alias, ScopedAlias};
11use calimero_primitives::application::ApplicationId;
12use calimero_primitives::blobs::{BlobId, BlobInfo, BlobMetadata};
13use calimero_primitives::context::ContextId;
14use calimero_primitives::hash::Hash;
15use calimero_primitives::identity::PublicKey;
16use calimero_server_primitives::admin::{
17 AliasKind, CreateAliasRequest, CreateAliasResponse, CreateApplicationIdAlias,
18 CreateContextIdAlias, CreateContextIdentityAlias, CreateContextRequest, CreateContextResponse,
19 DeleteAliasResponse, DeleteContextResponse, GenerateContextIdentityResponse,
20 GetApplicationResponse, GetContextClientKeysResponse, GetContextIdentitiesResponse,
21 GetContextResponse, GetContextStorageResponse, GetContextsResponse, GetLatestVersionResponse,
22 GetPeersCountResponse, GetProposalApproversResponse, GetProposalResponse, GetProposalsResponse,
23 GrantPermissionResponse, InstallApplicationRequest, InstallApplicationResponse,
24 InstallDevApplicationRequest, InviteSpecializedNodeRequest, InviteSpecializedNodeResponse,
25 InviteToContextOpenInvitationRequest, InviteToContextOpenInvitationResponse,
26 InviteToContextRequest, InviteToContextResponse, JoinContextByOpenInvitationRequest,
27 JoinContextRequest, JoinContextResponse, ListAliasesResponse, ListApplicationsResponse,
28 ListPackagesResponse, ListVersionsResponse, LookupAliasResponse, RevokePermissionResponse,
29 SyncContextResponse, UninstallApplicationResponse, UpdateContextApplicationRequest,
30 UpdateContextApplicationResponse,
31};
32use calimero_server_primitives::blob::{BlobDeleteResponse, BlobInfoResponse, BlobListResponse};
33use calimero_server_primitives::jsonrpc::{Request, Response};
34use eyre::Result;
35use serde::de::DeserializeOwned;
36use serde::Serialize;
37use url::Url;
38
39use crate::connection::ConnectionInfo;
41use crate::traits::{ClientAuthenticator, ClientStorage};
42
43pub trait UrlFragment: ScopedAlias + AliasKind {
44 const KIND: &'static str;
45
46 fn create(self) -> Self::Value;
47
48 fn scoped(scope: Option<&Self::Scope>) -> Option<&str>;
49}
50
51impl UrlFragment for ContextId {
52 const KIND: &'static str = "context";
53
54 fn create(self) -> Self::Value {
55 CreateContextIdAlias { context_id: self }
56 }
57
58 fn scoped(_: Option<&Self::Scope>) -> Option<&str> {
59 None
60 }
61}
62
63impl UrlFragment for PublicKey {
64 const KIND: &'static str = "identity";
65
66 fn create(self) -> Self::Value {
67 CreateContextIdentityAlias { identity: self }
68 }
69
70 fn scoped(context: Option<&Self::Scope>) -> Option<&str> {
71 context.map(ContextId::as_str)
72 }
73}
74
75impl UrlFragment for ApplicationId {
76 const KIND: &'static str = "application";
77
78 fn create(self) -> Self::Value {
79 CreateApplicationIdAlias {
80 application_id: self,
81 }
82 }
83
84 fn scoped(_: Option<&Self::Scope>) -> Option<&str> {
85 None
86 }
87}
88
89#[derive(Debug, Serialize)]
90pub struct ResolveResponse<T> {
91 alias: Alias<T>,
92 value: Option<ResolveResponseValue<T>>,
93}
94
95#[derive(Debug, Serialize)]
96#[serde(tag = "kind", content = "data")]
97pub enum ResolveResponseValue<T> {
98 Lookup(LookupAliasResponse<T>),
99 Parsed(T),
100}
101
102impl<T> ResolveResponse<T> {
103 pub fn value(&self) -> Option<&T> {
104 match self.value.as_ref()? {
105 ResolveResponseValue::Lookup(value) => value.data.value.as_ref(),
106 ResolveResponseValue::Parsed(value) => Some(value),
107 }
108 }
109
110 pub fn alias(&self) -> &Alias<T> {
111 &self.alias
112 }
113
114 pub fn value_enum(&self) -> Option<&ResolveResponseValue<T>> {
115 self.value.as_ref()
116 }
117}
118
119#[derive(Clone, Debug)]
121pub struct Client<A, S>
122where
123 A: ClientAuthenticator + Clone + Send + Sync,
124 S: ClientStorage + Clone + Send + Sync,
125{
126 connection: ConnectionInfo<A, S>,
127}
128
129impl<A, S> Client<A, S>
130where
131 A: ClientAuthenticator + Clone + Send + Sync,
132 S: ClientStorage + Clone + Send + Sync,
133{
134 pub fn new(connection: ConnectionInfo<A, S>) -> Result<Self> {
135 Ok(Self { connection })
136 }
137
138 pub fn api_url(&self) -> &Url {
139 &self.connection.api_url
140 }
141
142 pub async fn get_application(&self, app_id: &ApplicationId) -> Result<GetApplicationResponse> {
143 let response = self
144 .connection
145 .get(&format!("admin-api/applications/{app_id}"))
146 .await?;
147 Ok(response)
148 }
149
150 pub async fn install_dev_application(
151 &self,
152 request: InstallDevApplicationRequest,
153 ) -> Result<InstallApplicationResponse> {
154 let response = self
155 .connection
156 .post("admin-api/install-dev-application", request)
157 .await?;
158 Ok(response)
159 }
160
161 pub async fn install_application(
162 &self,
163 request: InstallApplicationRequest,
164 ) -> Result<InstallApplicationResponse> {
165 let response = self
166 .connection
167 .post("admin-api/install-application", request)
168 .await?;
169 Ok(response)
170 }
171
172 pub async fn list_applications(&self) -> Result<ListApplicationsResponse> {
173 let response = self.connection.get("admin-api/applications").await?;
174 Ok(response)
175 }
176
177 pub async fn uninstall_application(
178 &self,
179 app_id: &ApplicationId,
180 ) -> Result<UninstallApplicationResponse> {
181 let response = self
182 .connection
183 .delete(&format!("admin-api/applications/{app_id}"))
184 .await?;
185 Ok(response)
186 }
187
188 pub async fn delete_blob(&self, blob_id: &BlobId) -> Result<BlobDeleteResponse> {
189 let response = self
190 .connection
191 .delete(&format!("admin-api/blobs/{blob_id}"))
192 .await?;
193 Ok(response)
194 }
195
196 pub async fn list_blobs(&self) -> Result<BlobListResponse> {
197 let response = self.connection.get("admin-api/blobs").await?;
198 Ok(response)
199 }
200
201 pub async fn get_blob_info(&self, blob_id: &BlobId) -> Result<BlobInfoResponse> {
202 let headers = self
203 .connection
204 .head(&format!("admin-api/blobs/{blob_id}"))
205 .await?;
206
207 let size = headers
208 .get("content-length")
209 .and_then(|h| h.to_str().ok())
210 .and_then(|s| s.parse::<u64>().ok())
211 .unwrap_or(0);
212
213 let mime_type = headers
214 .get("content-type")
215 .and_then(|h| h.to_str().ok())
216 .unwrap_or("application/octet-stream")
217 .to_owned();
218
219 let hash_hex = headers
220 .get("x-blob-hash")
221 .and_then(|h| h.to_str().ok())
222 .unwrap_or("");
223
224 let hash =
225 hex::decode(hash_hex).map_err(|_| eyre::eyre!("Invalid hash in response headers"))?;
226
227 let hash_array: [u8; 32] = hash
228 .try_into()
229 .map_err(|_| eyre::eyre!("Hash must be 32 bytes"))?;
230
231 let blob_info = BlobInfoResponse {
232 data: BlobMetadata {
233 blob_id: *blob_id,
234 size,
235 mime_type,
236 hash: hash_array,
237 },
238 };
239
240 Ok(blob_info)
241 }
242
243 pub async fn upload_blob(
244 &self,
245 data: Vec<u8>,
246 context_id: Option<&ContextId>,
247 ) -> Result<BlobInfo> {
248 let path = if let Some(ctx_id) = context_id {
249 format!("admin-api/blobs?context_id={}", ctx_id)
250 } else {
251 "admin-api/blobs".to_owned()
252 };
253
254 let response = self.connection.put_binary(&path, data).await?;
255
256 #[derive(serde::Deserialize)]
257 struct BlobUploadResponse {
258 data: BlobInfo,
259 }
260
261 let upload_response: BlobUploadResponse = response.json().await?;
262 Ok(upload_response.data)
263 }
264
265 pub async fn download_blob(
266 &self,
267 blob_id: &BlobId,
268 context_id: Option<&ContextId>,
269 ) -> Result<Vec<u8>> {
270 let path = if let Some(ctx_id) = context_id {
271 format!("admin-api/blobs/{}?context_id={}", blob_id, ctx_id)
272 } else {
273 format!("admin-api/blobs/{}", blob_id)
274 };
275
276 let data = self.connection.get_binary(&path).await?;
277 Ok(data)
278 }
279
280 pub async fn generate_context_identity(&self) -> Result<GenerateContextIdentityResponse> {
281 let response = self
282 .connection
283 .post("admin-api/identity/context", ())
284 .await?;
285 Ok(response)
286 }
287
288 pub async fn get_peers_count(&self) -> Result<GetPeersCountResponse> {
289 let response = self.connection.get("admin-api/peers").await?;
290 Ok(response)
291 }
292
293 pub async fn execute_jsonrpc<P>(&self, request: Request<P>) -> Result<Response>
294 where
295 P: Serialize,
296 {
297 eprintln!(
299 "🔍 JSON-RPC Request to {}: {}",
300 self.connection.api_url.join("jsonrpc")?,
301 serde_json::to_string_pretty(&request)?
302 );
303
304 let response = self.connection.post("jsonrpc", request).await?;
305
306 eprintln!(
308 "🔍 JSON-RPC Parsed Response: {}",
309 serde_json::to_string_pretty(&response)?
310 );
311
312 Ok(response)
313 }
314
315 pub async fn grant_permissions(
316 &self,
317 context_id: &ContextId,
318 request: Vec<(PublicKey, calimero_context_config::types::Capability)>,
319 ) -> Result<GrantPermissionResponse> {
320 let response = self
321 .connection
322 .post(
323 &format!("admin-api/contexts/{}/capabilities/grant", context_id),
324 request,
325 )
326 .await?;
327 Ok(response)
328 }
329
330 pub async fn revoke_permissions(
331 &self,
332 context_id: &ContextId,
333 request: Vec<(PublicKey, calimero_context_config::types::Capability)>,
334 ) -> Result<RevokePermissionResponse> {
335 let response = self
336 .connection
337 .post(
338 &format!("admin-api/contexts/{}/capabilities/revoke", context_id),
339 request,
340 )
341 .await?;
342 Ok(response)
343 }
344
345 pub async fn invite_to_context(
346 &self,
347 request: InviteToContextRequest,
348 ) -> Result<InviteToContextResponse> {
349 let response = self
350 .connection
351 .post("admin-api/contexts/invite", request)
352 .await?;
353 Ok(response)
354 }
355
356 pub async fn invite_to_context_by_open_invitation(
357 &self,
358 request: InviteToContextOpenInvitationRequest,
359 ) -> Result<InviteToContextOpenInvitationResponse> {
360 let response = self
361 .connection
362 .post("admin-api/contexts/invite_by_open_invitation", request)
363 .await?;
364 Ok(response)
365 }
366
367 pub async fn invite_specialized_node(
372 &self,
373 request: InviteSpecializedNodeRequest,
374 ) -> Result<InviteSpecializedNodeResponse> {
375 let response = self
376 .connection
377 .post("admin-api/contexts/invite-specialized-node", request)
378 .await?;
379 Ok(response)
380 }
381
382 pub async fn update_context_application(
383 &self,
384 context_id: &ContextId,
385 request: UpdateContextApplicationRequest,
386 ) -> Result<UpdateContextApplicationResponse> {
387 let response = self
388 .connection
389 .post(
390 &format!("admin-api/contexts/{context_id}/application"),
391 request,
392 )
393 .await?;
394 Ok(response)
395 }
396
397 pub async fn get_proposal(
398 &self,
399 context_id: &ContextId,
400 proposal_id: &Hash,
401 ) -> Result<GetProposalResponse> {
402 let response = self
403 .connection
404 .get(&format!(
405 "admin-api/contexts/{}/proposals/{}",
406 context_id, proposal_id
407 ))
408 .await?;
409 Ok(response)
410 }
411
412 pub async fn get_proposal_approvers(
413 &self,
414 context_id: &ContextId,
415 proposal_id: &Hash,
416 ) -> Result<GetProposalApproversResponse> {
417 let response = self
418 .connection
419 .get(&format!(
420 "admin-api/contexts/{}/proposals/{}/approvals/users",
421 context_id, proposal_id
422 ))
423 .await?;
424 Ok(response)
425 }
426
427 pub async fn create_and_approve_proposal(
428 &self,
429 context_id: &ContextId,
430 request: calimero_server_primitives::admin::CreateAndApproveProposalRequest,
431 ) -> Result<calimero_server_primitives::admin::CreateAndApproveProposalResponse> {
432 let response = self
433 .connection
434 .post(
435 &format!("admin-api/contexts/{context_id}/proposals/create-and-approve"),
436 request,
437 )
438 .await?;
439 Ok(response)
440 }
441
442 pub async fn approve_proposal(
443 &self,
444 context_id: &ContextId,
445 request: calimero_server_primitives::admin::ApproveProposalRequest,
446 ) -> Result<calimero_server_primitives::admin::ApproveProposalResponse> {
447 let response = self
448 .connection
449 .post(
450 &format!("admin-api/contexts/{context_id}/proposals/approve"),
451 request,
452 )
453 .await?;
454 Ok(response)
455 }
456
457 pub async fn list_proposals(
458 &self,
459 context_id: &ContextId,
460 args: serde_json::Value,
461 ) -> Result<GetProposalsResponse> {
462 let response = self
463 .connection
464 .post(&format!("admin-api/contexts/{context_id}/proposals"), args)
465 .await?;
466 Ok(response)
467 }
468
469 pub async fn get_context(&self, context_id: &ContextId) -> Result<GetContextResponse> {
470 let response = self
471 .connection
472 .get(&format!("admin-api/contexts/{context_id}"))
473 .await?;
474 Ok(response)
475 }
476
477 pub async fn list_contexts(&self) -> Result<GetContextsResponse> {
478 let response = self.connection.get("admin-api/contexts").await?;
479 Ok(response)
480 }
481
482 pub async fn create_context(
483 &self,
484 request: CreateContextRequest,
485 ) -> Result<CreateContextResponse> {
486 let response = self.connection.post("admin-api/contexts", request).await?;
487 Ok(response)
488 }
489
490 pub async fn delete_context(&self, context_id: &ContextId) -> Result<DeleteContextResponse> {
491 let response = self
492 .connection
493 .delete(&format!("admin-api/contexts/{context_id}"))
494 .await?;
495 Ok(response)
496 }
497
498 pub async fn join_context(&self, request: JoinContextRequest) -> Result<JoinContextResponse> {
499 let response = self
500 .connection
501 .post("admin-api/contexts/join", request)
502 .await?;
503 Ok(response)
504 }
505
506 pub async fn join_context_by_open_invitation(
507 &self,
508 request: JoinContextByOpenInvitationRequest,
509 ) -> Result<JoinContextResponse> {
510 let response = self
511 .connection
512 .post("admin-api/contexts/join_by_open_invitation", request)
513 .await?;
514 Ok(response)
515 }
516
517 pub async fn get_context_storage(
518 &self,
519 context_id: &ContextId,
520 ) -> Result<GetContextStorageResponse> {
521 let response = self
522 .connection
523 .get(&format!("admin-api/contexts/{context_id}/storage"))
524 .await?;
525 Ok(response)
526 }
527
528 pub async fn get_context_identities(
529 &self,
530 context_id: &ContextId,
531 owned: bool,
532 ) -> Result<GetContextIdentitiesResponse> {
533 let endpoint = if owned {
534 format!("admin-api/contexts/{}/identities-owned", context_id)
535 } else {
536 format!("admin-api/contexts/{}/identities", context_id)
537 };
538
539 let response = self.connection.get(&endpoint).await?;
540 Ok(response)
541 }
542
543 pub async fn get_context_client_keys(
544 &self,
545 context_id: &ContextId,
546 ) -> Result<GetContextClientKeysResponse> {
547 let response = self
548 .connection
549 .get(&format!("admin-api/contexts/{context_id}/client-keys"))
550 .await?;
551 Ok(response)
552 }
553
554 pub async fn sync_context(&self, context_id: &ContextId) -> Result<SyncContextResponse> {
555 let response = self
556 .connection
557 .post_no_body(&format!("admin-api/contexts/sync/{context_id}"))
558 .await?;
559 Ok(response)
560 }
561
562 pub async fn sync_all_contexts(&self) -> Result<SyncContextResponse> {
564 let response = self
565 .connection
566 .post_no_body("admin-api/contexts/sync")
567 .await?;
568 Ok(response)
569 }
570
571 pub async fn create_context_identity_alias(
573 &self,
574 context_id: &ContextId,
575 request: CreateAliasRequest<PublicKey>,
576 ) -> Result<CreateAliasResponse> {
577 let response = self
578 .connection
579 .post(
580 &format!("admin-api/alias/create/identity/{}", context_id),
581 request,
582 )
583 .await?;
584 Ok(response)
585 }
586
587 pub async fn create_alias_generic<T>(
589 &self,
590 alias: Alias<T>,
591 scope: Option<T::Scope>,
592 value: T,
593 ) -> Result<CreateAliasResponse>
594 where
595 T: UrlFragment + Serialize,
596 T::Value: Serialize,
597 {
598 self.create_alias(alias, value, scope).await
599 }
600
601 pub async fn create_alias<T>(
602 &self,
603 alias: Alias<T>,
604 value: T,
605 scope: Option<T::Scope>,
606 ) -> Result<CreateAliasResponse>
607 where
608 T: UrlFragment + Serialize,
609 T::Value: Serialize,
610 {
611 let prefix = "admin-api/alias/create";
612 let kind = T::KIND;
613 let scope_path = T::scoped(scope.as_ref())
614 .map(|scope| format!("/{}", scope))
615 .unwrap_or_default();
616
617 let body = CreateAliasRequest {
618 alias,
619 value: value.create(),
620 };
621
622 let response = self
623 .connection
624 .post(&format!("{prefix}/{kind}{scope_path}"), body)
625 .await?;
626 Ok(response)
627 }
628
629 pub async fn delete_alias<T>(
630 &self,
631 alias: Alias<T>,
632 scope: Option<T::Scope>,
633 ) -> Result<DeleteAliasResponse>
634 where
635 T: UrlFragment,
636 {
637 let prefix = "admin-api/alias/delete";
638 let kind = T::KIND;
639 let scope_path = T::scoped(scope.as_ref())
640 .map(|scope| format!("/{}", scope))
641 .unwrap_or_default();
642
643 let response = self
644 .connection
645 .post_no_body(&format!("{prefix}/{kind}{scope_path}/{alias}"))
646 .await?;
647 Ok(response)
648 }
649
650 pub async fn list_aliases<T>(&self, scope: Option<T::Scope>) -> Result<ListAliasesResponse<T>>
651 where
652 T: Ord + UrlFragment + DeserializeOwned,
653 {
654 let prefix = "admin-api/alias/list";
655 let kind = T::KIND;
656 let scope_path = T::scoped(scope.as_ref())
657 .map(|scope| format!("/{}", scope))
658 .unwrap_or_default();
659
660 let response = self
661 .connection
662 .get(&format!("{prefix}/{kind}{scope_path}"))
663 .await?;
664 Ok(response)
665 }
666
667 pub async fn lookup_alias<T>(
668 &self,
669 alias: Alias<T>,
670 scope: Option<T::Scope>,
671 ) -> Result<LookupAliasResponse<T>>
672 where
673 T: UrlFragment + DeserializeOwned,
674 {
675 let prefix = "admin-api/alias/lookup";
676 let kind = T::KIND;
677 let scope_path = T::scoped(scope.as_ref())
678 .map(|scope| format!("/{}", scope))
679 .unwrap_or_default();
680
681 let response = self
682 .connection
683 .post_no_body(&format!("{prefix}/{kind}{scope_path}/{alias}"))
684 .await?;
685 Ok(response)
686 }
687
688 pub async fn resolve_alias<T>(
689 &self,
690 alias: Alias<T>,
691 scope: Option<T::Scope>,
692 ) -> Result<ResolveResponse<T>>
693 where
694 T: UrlFragment + FromStr + DeserializeOwned,
695 {
696 let value = self.lookup_alias(alias, scope).await?;
697
698 if value.data.value.is_some() {
699 return Ok(ResolveResponse {
700 alias,
701 value: Some(ResolveResponseValue::Lookup(value)),
702 });
703 }
704
705 let value = alias
706 .as_str()
707 .parse()
708 .ok()
709 .map(ResolveResponseValue::Parsed);
710
711 Ok(ResolveResponse { alias, value })
712 }
713
714 pub async fn list_packages(&self) -> Result<ListPackagesResponse> {
716 let response = self.connection.get("admin-api/packages").await?;
717 Ok(response)
718 }
719
720 pub async fn list_versions(&self, package: &str) -> Result<ListVersionsResponse> {
721 let response = self
722 .connection
723 .get(&format!("admin-api/packages/{package}/versions"))
724 .await?;
725 Ok(response)
726 }
727
728 pub async fn get_latest_version(&self, package: &str) -> Result<GetLatestVersionResponse> {
729 let response = self
730 .connection
731 .get(&format!("admin-api/packages/{package}/latest"))
732 .await?;
733 Ok(response)
734 }
735}