1use anyhow::{Context, Result};
3use nostr_sdk::{Client, Keys, Filter, Kind, RelayPoolNotification, Url, EventBuilder, Tag, ToBech32, Timestamp};
4use nostr_sdk::nips::nip59::UnwrappedGift;
5use nostr_sdk::nips::nip04;
6use serde::{Deserialize, Serialize};
7use std::pin::Pin;
8use std::future::Future;
9use std::sync::Arc;
10use tokio::sync::Mutex;
11use tracing::{debug, error, info, warn};
12
13pub const KIND_PROVIDER_OFFER: u16 = 38383;
15pub const KIND_PROVIDER_HEARTBEAT: u16 = 38384;
16#[derive(Clone, Debug)]
17pub struct RelayConfig {
18 pub relays: Vec<String>,
19 pub private_key: Option<String>,
20}
21
22#[derive(Debug, Clone, Deserialize, Serialize)]
23pub struct NostrEvent {
24 pub id: String,
25 pub pubkey: String,
26 pub created_at: u64,
27 pub kind: u32,
28 pub tags: Vec<Vec<String>>,
29 pub content: String,
30 pub sig: String,
31 pub message_type: String, }
33
34
35#[derive(Clone)]
36pub struct NostrRelaySubscriber {
37 client: Client,
38 keys: Keys,
39 }
41
42impl NostrRelaySubscriber {
43 pub async fn new(config: RelayConfig) -> Result<Self> {
44 let keys = match &config.private_key {
45 Some(private_key_hex) if !private_key_hex.is_empty() => {
46 if private_key_hex.starts_with("nsec1") {
48 Keys::parse(private_key_hex)
49 .context("Invalid nsec private key format")?
50 } else {
51 Keys::parse(private_key_hex)
53 .context("Invalid private key format")?
54 }
55 }
56 _ => {
57 Keys::generate()
59 }
60 };
61
62 let client = Client::new(&keys);
63
64 for relay_url in &config.relays {
66 info!("Adding relay: {}", relay_url);
67 let url = Url::parse(relay_url)
68 .with_context(|| format!("Invalid relay URL: {}", relay_url))?;
69 client.add_relay(url).await?;
70 }
71
72 info!("Connecting to {} relays...", config.relays.len());
73 client.connect().await;
74
75 tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
77
78 info!("Connected to {} relays", config.relays.len());
79 info!("Service public key (npub): {}", keys.public_key().to_bech32().unwrap());
80
81 Ok(Self { client, keys })
82 }
83
84 pub fn public_key(&self) -> nostr_sdk::PublicKey {
85 self.keys.public_key()
86 }
87
88 pub async fn subscribe_to_pod_events<F>(&self, handler: F) -> Result<()>
89 where
90 F: Fn(NostrEvent) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send>> + Send + Sync + 'static,
91 {
92 let nip04_filter = Filter::new()
94 .kind(Kind::EncryptedDirectMessage)
95 .pubkeys(vec![self.keys.public_key()]) .limit(0);
97
98 let nip17_filter = Filter::new()
99 .kind(Kind::GiftWrap)
100 .pubkeys(vec![self.keys.public_key()]) .limit(0);
102
103 let _ = self.client.subscribe(vec![nip04_filter, nip17_filter], None).await;
104 info!("Subscribed to NIP-04 (Encrypted Direct Messages) and NIP-17 (Gift Wrap) messages for pod provisioning and top-up requests");
105
106 self.client.handle_notifications(|notification| async {
108 if let RelayPoolNotification::Event { relay_url: _, subscription_id: _, event } = notification {
109 match event.kind {
110 Kind::GiftWrap => {
111 info!("Received NIP-17 Gift Wrap message: {}", event.id);
112
113 match self.client.unwrap_gift_wrap(&event).await {
115 Ok(UnwrappedGift { rumor, sender }) => {
116 info!("Unwrapped Gift Wrap from sender: {}, rumor kind: {}", sender, rumor.kind);
117
118 if rumor.kind == Kind::PrivateDirectMessage {
120 debug!("NIP-17 rumor is PrivateDirectMessage. Content length: {}", rumor.content.len());
121
122 let nostr_event = NostrEvent {
124 id: rumor.id.map(|id| id.to_hex()).unwrap_or_else(|| "unknown".to_string()),
125 pubkey: rumor.pubkey.to_hex(),
126 created_at: rumor.created_at.as_u64(),
127 kind: rumor.kind.as_u32(),
128 tags: rumor.tags.iter().map(|tag| {
129 tag.as_vec().iter().map(|s| s.to_string()).collect()
130 }).collect(),
131 content: rumor.content,
132 sig: "unsigned".to_string(), message_type: "nip17".to_string(), };
135
136 match handler(nostr_event).await {
137 Ok(()) => {
138 info!("Successfully processed NIP-17 private message: {}", event.id);
139 }
140 Err(e) => {
141 error!("Failed to process NIP-17 private message {}: {}", event.id, e);
142 }
143 }
144 } else {
145 info!("Rumor is not a private direct message, kind: {}", rumor.kind);
146 }
147 }
148 Err(e) => {
149 error!("Failed to unwrap Gift Wrap {}: {}", event.id, e);
150 }
151 }
152 }
153 Kind::EncryptedDirectMessage => {
154 info!("Received NIP-04 Encrypted Direct Message: {}", event.id);
155
156 match self.keys.secret_key() {
158 Ok(secret_key) => {
159 match nip04::decrypt(&secret_key, &event.pubkey, &event.content) {
160 Ok(decrypted_content) => {
161 debug!("Decrypted NIP-04 message. Length: {}", decrypted_content.len());
162
163 let nostr_event = NostrEvent {
165 id: event.id.to_hex(),
166 pubkey: event.pubkey.to_hex(),
167 created_at: event.created_at.as_u64(),
168 kind: event.kind.as_u32(),
169 tags: event.tags.iter().map(|tag| {
170 tag.as_vec().iter().map(|s| s.to_string()).collect()
171 }).collect(),
172 content: decrypted_content,
173 sig: event.sig.to_string(),
174 message_type: "nip04".to_string(), };
176
177 match handler(nostr_event).await {
178 Ok(()) => {
179 info!("Successfully processed NIP-04 private message: {}", event.id);
180 }
181 Err(e) => {
182 error!("Failed to process NIP-04 private message {}: {}", event.id, e);
183 }
184 }
185 }
186 Err(e) => {
187 error!("Failed to decrypt NIP-04 message {}: {}", event.id, e);
188 }
189 }
190 }
191 Err(e) => {
192 error!("Failed to get secret key for NIP-04 decryption: {}", e);
193 }
194 }
195 }
196 _ => {
197 info!("Received unsupported event kind: {}", event.kind);
198 }
199 }
200 }
201 Ok(false) }).await?;
203
204 Ok(())
205 }
206
207 pub async fn publish_offer(&self, offer: OfferEventContent) -> Result<String> {
208 let content = serde_json::to_string(&offer)?;
209 info!("Publishing offer event with content: {}", content);
210
211 let tags = vec![
212 Tag::hashtag("paygress"),
213 Tag::hashtag("offer"),
214 ];
215
216 info!("Creating event with kind 999 and {} tags", tags.len());
217 let builder = EventBuilder::new(Kind::Custom(999), content, tags);
218 let event = builder.to_event(&self.keys)?;
219 let event_id = event.id.to_hex();
220
221 info!("Event created with ID: {}", event_id);
222 info!("Sending offer event to relays: {}", event_id);
223
224 match self.client.send_event(event).await {
225 Ok(res) => {
226 info!("✅ Successfully published offer event: {} and {:?}", event_id, res);
227 Ok(event_id)
228 }
229 Err(e) => {
230 error!("❌ Failed to send offer event: {}", e);
231 Err(e.into())
232 }
233 }
234 }
235
236 pub async fn send_encrypted_private_message(
238 &self,
239 receiver_pubkey: &str,
240 content: String,
241 message_type: &str,
242 ) -> Result<String> {
243 let receiver_pubkey_parsed = nostr_sdk::PublicKey::parse(receiver_pubkey)?;
244
245 match message_type {
246 "nip04" => {
247 match self.keys.secret_key() {
248 Ok(secret_key) => {
249 let encrypted_content = nip04::encrypt(&secret_key, &receiver_pubkey_parsed, &content)?;
250 let receiver_tag = Tag::public_key(receiver_pubkey_parsed);
251 let alt_tag = Tag::parse(&["alt", "Private Message"])?;
252
253 let event_builder = EventBuilder::new(Kind::EncryptedDirectMessage, encrypted_content, [receiver_tag, alt_tag]);
254 let event = event_builder.to_event(&self.keys)?;
255 let event_id = self.client.send_event(event).await?;
256 info!("Sent NIP-04 message to {}: {:?}", receiver_pubkey, event_id);
257 Ok(event_id.to_hex())
258 }
259 Err(e) => {
260 error!("Failed to get secret key for NIP-04 encryption: {}", e);
261 Err(e.into())
262 }
263 }
264 }
265 "nip17" | _ => {
266 let event_id = self.client.send_private_msg(receiver_pubkey_parsed, content, None).await?;
268 info!("Sent NIP-17 message to {}: {:?}", receiver_pubkey, event_id);
269 Ok(event_id.to_hex())
270 }
271 }
272 }
273
274 pub async fn send_access_details_private_message(
276 &self,
277 request_pubkey: &str,
278 details: AccessDetailsContent,
279 message_type: &str
280 ) -> Result<String> {
281 let details_json = serde_json::to_string(&details)?;
282 self.send_encrypted_private_message(request_pubkey, details_json, message_type).await
283 }
284
285 pub async fn send_status_response(
287 &self,
288 request_pubkey: &str,
289 response: StatusResponseContent,
290 message_type: &str
291 ) -> Result<String> {
292 let response_json = serde_json::to_string(&response)?;
293 self.send_encrypted_private_message(request_pubkey, response_json, message_type).await
294 }
295
296 pub async fn send_error_response(
298 &self,
299 request_pubkey: &str,
300 error_type: &str,
301 message: &str,
302 details: Option<&str>,
303 message_type: &str,
304 ) -> Result<String> {
305 let error = ErrorResponseContent {
306 error_type: error_type.to_string(),
307 message: message.to_string(),
308 details: details.map(|s| s.to_string()),
309 };
310 self.send_error_response_private_message(request_pubkey, error, message_type).await
311 }
312
313 pub async fn send_error_response_private_message(
315 &self,
316 request_pubkey: &str,
317 error: ErrorResponseContent,
318 message_type: &str
319 ) -> Result<String> {
320 let error_json = serde_json::to_string(&error)?;
321 self.send_encrypted_private_message(request_pubkey, error_json, message_type).await
322 }
323
324 pub async fn send_topup_response_private_message(
326 &self,
327 request_pubkey: &str,
328 response: TopUpResponseContent,
329 message_type: &str
330 ) -> Result<String> {
331 let response_json = serde_json::to_string(&response)?;
332 self.send_encrypted_private_message(request_pubkey, response_json, message_type).await
333 }
334
335
336
337 pub fn client(&self) -> &Client {
339 &self.client
340 }
341
342 pub fn get_service_public_key(&self) -> String {
344 self.keys.public_key().to_hex()
345 }
346
347 fn convert_event(&self, event: &nostr_sdk::Event) -> NostrEvent {
348 NostrEvent {
349 id: event.id.to_hex(),
350 pubkey: event.pubkey.to_hex(),
351 created_at: event.created_at.as_u64(),
352 kind: event.kind.as_u32(),
353 tags: event.tags.iter().map(|tag| {
354 tag.as_vec().iter().map(|s| s.to_string()).collect()
355 }).collect(),
356 content: event.content.clone(),
357 sig: event.sig.to_string(),
358 message_type: "unknown".to_string(), }
360 }
361
362 pub async fn wait_for_decrypted_message(&self, sender_pubkey: &str, timeout_secs: u64) -> Result<NostrEvent> {
364 let sender_pk = nostr_sdk::PublicKey::parse(sender_pubkey)?;
365 let receiver_pk = self.keys.public_key();
366
367 let (tx, mut rx) = tokio::sync::mpsc::channel(1);
368 let tx = Arc::new(Mutex::new(Some(tx)));
369 let client = self.client.clone();
370 let receiver_keys = self.keys.clone();
371 let timeout = tokio::time::Duration::from_secs(timeout_secs);
372
373 let filter = Filter::new()
375 .pubkeys(vec![receiver_pk])
376 .kinds(vec![Kind::EncryptedDirectMessage, Kind::GiftWrap]);
377
378 let _ = client.subscribe(vec![filter], None).await;
379
380 let result = tokio::select! {
382 notification_res = client.handle_notifications(|notification| {
383 let tx = tx.clone();
384 let receiver_keys = receiver_keys.clone();
385 let sender_pk = sender_pk.clone();
386 let client = client.clone();
387
388 async move {
389 if let RelayPoolNotification::Event { event, .. } = notification {
390 let mut event_to_send = None;
391
392 match event.kind {
393 Kind::GiftWrap => {
394 if let Ok(UnwrappedGift { rumor, sender }) = client.unwrap_gift_wrap(&event).await {
396 if sender == sender_pk && rumor.kind == Kind::PrivateDirectMessage {
397 event_to_send = Some(NostrEvent {
398 id: rumor.id.map(|id| id.to_hex()).unwrap_or_default(),
399 pubkey: sender.to_hex(),
400 created_at: rumor.created_at.as_u64(),
401 kind: rumor.kind.as_u32(),
402 tags: rumor.tags.iter().map(|tag| tag.as_vec().iter().map(|s| s.to_string()).collect()).collect(),
403 content: rumor.content,
404 sig: String::new(),
405 message_type: "nip17".to_string(),
406 });
407 }
408 }
409 }
410 Kind::EncryptedDirectMessage => {
411 if event.pubkey == sender_pk {
412 if let Ok(secret_key) = receiver_keys.secret_key() {
413 if let Ok(content) = nip04::decrypt(&secret_key, &event.pubkey, &event.content) {
414 event_to_send = Some(NostrEvent {
415 id: event.id.to_hex(),
416 pubkey: event.pubkey.to_hex(),
417 created_at: event.created_at.as_u64(),
418 kind: event.kind.as_u32(),
419 tags: event.tags.iter().map(|tag| tag.as_vec().iter().map(|s| s.to_string()).collect()).collect(),
420 content,
421 sig: event.sig.to_string(),
422 message_type: "nip04".to_string(),
423 });
424 }
425 }
426 }
427 }
428 _ => {}
429 }
430
431 if let Some(ev) = event_to_send {
432 let mut lock = tx.lock().await;
433 if let Some(sender) = lock.take() {
434 let _ = sender.send(ev).await;
435 return Ok(true); }
437 }
438 }
439 Ok(false)
440 }
441 }) => {
442 match notification_res {
443 Ok(_) => rx.recv().await.ok_or_else(|| anyhow::anyhow!("Channel closed")),
444 Err(e) => Err(anyhow::anyhow!("Notification handler error: {}", e)),
445 }
446 }
447 _ = tokio::time::sleep(timeout) => {
448 Err(anyhow::anyhow!("Timeout waiting for response from {}", sender_pubkey))
449 }
450 };
451
452 result
453 }
454}
455
456pub fn default_relay_config() -> RelayConfig {
457 RelayConfig {
458 relays: vec![
459 "wss://relay.damus.io".to_string(),
460 "wss://nos.lol".to_string(),
461 "wss://relay.nostr.band".to_string(),
462 ],
463 private_key: None,
464 }
465}
466
467pub fn custom_relay_config(relays: Vec<String>, private_key: Option<String>) -> RelayConfig {
468 RelayConfig { relays, private_key }
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct PodSpec {
473 pub id: String, pub name: String, pub description: String, pub cpu_millicores: u64, pub memory_mb: u64, pub rate_msats_per_sec: u64, }
480
481#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct OfferEventContent {
483 pub minimum_duration_seconds: u64,
484 pub whitelisted_mints: Vec<String>,
485 pub pod_specs: Vec<PodSpec>, }
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct AccessDetailsContent {
490 pub pod_npub: String, pub node_port: u16, pub expires_at: String, pub cpu_millicores: u64, pub memory_mb: u64, pub pod_spec_name: String, pub pod_spec_description: String, pub instructions: Vec<String>, }
499
500#[derive(Debug, Clone, Serialize, Deserialize)]
501pub struct ErrorResponseContent {
502 pub error_type: String, pub message: String, pub details: Option<String>, }
506
507#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct TopUpResponseContent {
509 pub success: bool,
510 pub pod_npub: String,
511 pub extended_duration_seconds: u64,
512 pub new_expires_at: String,
513 pub message: String,
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize)]
518pub struct EncryptedSpawnPodRequest {
519 pub cashu_token: String,
520 pub pod_spec_id: Option<String>, pub pod_image: String, pub ssh_username: String,
523 pub ssh_password: String,
524}
525
526#[derive(Debug, Clone, Serialize, Deserialize)]
528pub struct EncryptedTopUpPodRequest {
529 pub pod_npub: String, pub cashu_token: String,
531}
532
533pub async fn send_provisioning_request_private_message(
535 client: &Client,
536 service_pubkey: &str,
537 request: EncryptedSpawnPodRequest,
538) -> Result<String> {
539 let request_json = serde_json::to_string(&request)?;
540
541 let service_pubkey_parsed = nostr_sdk::PublicKey::parse(service_pubkey)?;
543 let event_id = client.send_private_msg(service_pubkey_parsed, request_json, None).await?;
544
545 Ok(event_id.to_hex())
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize)]
551#[serde(untagged)]
552pub enum PrivateRequest {
553 Spawn(EncryptedSpawnPodRequest),
554 TopUp(EncryptedTopUpPodRequest),
555 Status(StatusRequestContent),
556}
557
558pub fn parse_private_message_content(content: &str) -> Result<PrivateRequest> {
559 match serde_json::from_str::<PrivateRequest>(content) {
560 Ok(request) => Ok(request),
561 Err(e) => {
562 let truncated_content = if content.len() > 100 {
564 format!("{}...", &content[..100])
565 } else {
566 content.to_string()
567 };
568 Err(anyhow::anyhow!("JSON parsing failed: {}. Content: '{}'", e, truncated_content))
569 }
570 }
571}
572
573#[derive(Debug, Clone, Serialize, Deserialize)]
577pub struct CapacityInfo {
578 pub cpu_available: u64, pub memory_mb_available: u64, pub storage_gb_available: u64, }
582
583#[derive(Debug, Clone, Serialize, Deserialize)]
586pub struct ProviderOfferContent {
587 pub provider_npub: String,
588 pub hostname: String,
589 pub location: Option<String>,
590 pub capabilities: Vec<String>, pub specs: Vec<PodSpec>,
592 pub whitelisted_mints: Vec<String>,
593 pub uptime_percent: f32,
594 pub total_jobs_completed: u64,
595 pub api_endpoint: Option<String>,
596}
597
598#[derive(Debug, Clone, Serialize, Deserialize)]
601pub struct HeartbeatContent {
602 pub provider_npub: String,
603 pub timestamp: u64,
604 pub active_workloads: u32,
605 pub available_capacity: CapacityInfo,
606}
607
608#[derive(Debug, Clone, Serialize, Deserialize)]
610pub struct ProviderInfo {
611 pub npub: String,
612 pub hostname: String,
613 pub location: Option<String>,
614 pub capabilities: Vec<String>,
615 pub specs: Vec<PodSpec>,
616 pub whitelisted_mints: Vec<String>,
617 pub uptime_percent: f32,
618 pub total_jobs_completed: u64,
619 pub last_seen: u64, pub is_online: bool,
621}
622
623#[derive(Debug, Clone, Default)]
625pub struct ProviderFilter {
626 pub capability: Option<String>,
627 pub min_uptime: Option<f32>,
628 pub min_memory_mb: Option<u64>,
629 pub min_cpu: Option<u64>,
630}
631
632impl NostrRelaySubscriber {
633 pub async fn publish_provider_offer(&self, offer: ProviderOfferContent) -> Result<String> {
635 let content = serde_json::to_string(&offer)?;
636 info!("Publishing provider offer for {}", offer.hostname);
637
638 let tags = vec![
640 Tag::hashtag("paygress"),
641 Tag::hashtag("compute"),
642 Tag::parse(&["d", &offer.provider_npub])?,
643 ];
644
645 let builder = EventBuilder::new(Kind::Custom(KIND_PROVIDER_OFFER), content, tags);
646 let event = builder.to_event(&self.keys)?;
647 let event_id = event.id.to_hex();
648
649 match self.client.send_event(event).await {
650 Ok(res) => {
651 info!("✅ Published provider offer: {} ({:?})", event_id, res);
652 Ok(event_id)
653 }
654 Err(e) => {
655 error!("❌ Failed to publish provider offer: {}", e);
656 Err(e.into())
657 }
658 }
659 }
660
661 pub async fn publish_heartbeat(&self, heartbeat: HeartbeatContent) -> Result<String> {
663 let content = serde_json::to_string(&heartbeat)?;
664
665 let tags = vec![
666 Tag::hashtag("paygress-heartbeat"),
667 Tag::public_key(nostr_sdk::PublicKey::parse(&heartbeat.provider_npub)?),
668 ];
669
670 let builder = EventBuilder::new(Kind::Custom(KIND_PROVIDER_HEARTBEAT), content, tags);
671 let event = builder.to_event(&self.keys)?;
672 let event_id = event.id.to_hex();
673
674 match self.client.send_event(event).await {
675 Ok(_) => {
676 info!("💓 Heartbeat published: {}", event_id);
677 Ok(event_id)
678 }
679 Err(e) => {
680 warn!("Failed to publish heartbeat: {}", e);
681 Err(e.into())
682 }
683 }
684 }
685
686 pub async fn query_providers(&self) -> Result<Vec<ProviderOfferContent>> {
688 let filter = Filter::new()
689 .kind(Kind::Custom(KIND_PROVIDER_OFFER))
690 .hashtag("paygress");
691
692 let events = self.client.get_events_of(vec![filter], Some(std::time::Duration::from_secs(5))).await?;
693
694 let mut providers = Vec::new();
695 for event in events {
696 match serde_json::from_str::<ProviderOfferContent>(&event.content) {
697 Ok(offer) => providers.push(offer),
698 Err(e) => {
699 warn!("Failed to parse provider offer {}: {}", event.id, e);
700 }
701 }
702 }
703
704 info!("Found {} providers", providers.len());
705 Ok(providers)
706 }
707
708 pub async fn query_heartbeats(&self, provider_npub: &str, since_secs: u64) -> Result<Vec<HeartbeatContent>> {
710 let provider_pubkey = nostr_sdk::PublicKey::parse(provider_npub)?;
711
712 let filter = Filter::new()
713 .kind(Kind::Custom(KIND_PROVIDER_HEARTBEAT))
714 .author(provider_pubkey)
715 .since(Timestamp::from(since_secs));
716
717 let events = self.client.get_events_of(vec![filter], Some(std::time::Duration::from_secs(5))).await?;
718
719 let mut heartbeats = Vec::new();
720 for event in events {
721 match serde_json::from_str::<HeartbeatContent>(&event.content) {
722 Ok(hb) => heartbeats.push(hb),
723 Err(e) => {
724 warn!("Failed to parse heartbeat {}: {}", event.id, e);
725 }
726 }
727 }
728
729 Ok(heartbeats)
730 }
731
732 pub async fn get_latest_heartbeat(&self, provider_npub: &str) -> Result<Option<HeartbeatContent>> {
734 let provider_pubkey = nostr_sdk::PublicKey::parse(provider_npub)?;
735
736 let five_mins_ago = std::time::SystemTime::now()
738 .duration_since(std::time::UNIX_EPOCH)?
739 .as_secs() - 300;
740
741 let filter = Filter::new()
742 .kind(Kind::Custom(KIND_PROVIDER_HEARTBEAT))
743 .author(provider_pubkey)
744 .since(Timestamp::from(five_mins_ago))
745 .limit(1);
746
747 let events = self.client.get_events_of(vec![filter], Some(std::time::Duration::from_secs(3))).await?;
748
749 if let Some(event) = events.first() {
750 match serde_json::from_str::<HeartbeatContent>(&event.content) {
751 Ok(hb) => return Ok(Some(hb)),
752 Err(e) => warn!("Failed to parse heartbeat: {}", e),
753 }
754 }
755
756 Ok(None)
757 }
758
759 pub async fn get_latest_heartbeats_multi(&self, provider_npubs: Vec<String>) -> Result<std::collections::HashMap<String, HeartbeatContent>> {
761 if provider_npubs.is_empty() {
762 return Ok(std::collections::HashMap::new());
763 }
764
765 let mut pubkeys = Vec::new();
766 for npub in provider_npubs {
767 if let Ok(pk) = nostr_sdk::PublicKey::parse(&npub) {
768 pubkeys.push(pk);
769 }
770 }
771
772 let five_mins_ago = std::time::SystemTime::now()
774 .duration_since(std::time::UNIX_EPOCH)?
775 .as_secs() - 300;
776
777 let filter = Filter::new()
779 .kind(Kind::Custom(KIND_PROVIDER_HEARTBEAT))
780 .authors(pubkeys)
781 .since(Timestamp::from(five_mins_ago));
782
783 let events = self.client.get_events_of(vec![filter], Some(std::time::Duration::from_secs(3))).await?;
785
786 let mut heartbeats = std::collections::HashMap::new();
787
788 for event in events {
790 if let Ok(hb) = serde_json::from_str::<HeartbeatContent>(&event.content) {
791 match heartbeats.entry(hb.provider_npub.clone()) {
792 std::collections::hash_map::Entry::Occupied(mut entry) => {
793 let existing: &HeartbeatContent = entry.get();
794 if hb.timestamp > existing.timestamp {
795 entry.insert(hb);
796 }
797 }
798 std::collections::hash_map::Entry::Vacant(entry) => {
799 entry.insert(hb);
800 }
801 }
802 }
803 }
804
805 Ok(heartbeats)
806 }
807
808 pub async fn calculate_uptime(&self, provider_npub: &str, days: u32) -> Result<f32> {
810 let now = std::time::SystemTime::now()
811 .duration_since(std::time::UNIX_EPOCH)?
812 .as_secs();
813 let since = now - (days as u64 * 24 * 60 * 60);
814
815 let heartbeats = self.query_heartbeats(provider_npub, since).await?;
816
817 if heartbeats.is_empty() {
818 return Ok(0.0);
819 }
820
821 let expected = (days as f32) * 24.0 * 60.0;
823 let actual = heartbeats.len() as f32;
824
825 Ok((actual / expected * 100.0).min(100.0))
826 }
827}
828
829#[derive(Debug, Clone, Serialize, Deserialize)]
830pub struct StatusRequestContent {
831 pub pod_id: String, }
833
834#[derive(Debug, Clone, Serialize, Deserialize)]
835pub struct StatusResponseContent {
836 pub pod_id: String,
837 pub status: String,
838 pub expires_at: String,
839 pub time_remaining_seconds: u64,
840 pub cpu_millicores: u64,
841 pub memory_mb: u64,
842 pub ssh_host: String,
843 pub ssh_port: u16,
844 pub ssh_username: String,
845}