1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5use crate::config::NetworkConfig;
6
7pub struct ApiClient {
8 client: reqwest::Client,
9 base_url: String,
10}
11
12#[derive(Debug, Serialize)]
13pub struct HttpsCallRequest {
14 pub input: Value,
15 #[serde(rename = "async")]
16 pub is_async: bool,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub version_key: Option<String>,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub secrets_ref: Option<SecretsRef>,
21}
22
23#[derive(Debug, Clone, Serialize)]
24pub struct SecretsRef {
25 pub profile: String,
26 pub account_id: String,
27}
28
29#[derive(Debug, Deserialize)]
30pub struct HttpsCallResponse {
31 pub call_id: String,
32 pub status: String,
33 pub output: Option<Value>,
34 pub error: Option<String>,
35 pub compute_cost: Option<String>,
36 #[allow(dead_code)]
37 pub instructions: Option<u64>,
38 pub time_ms: Option<u64>,
39 pub poll_url: Option<String>,
40 #[allow(dead_code)]
41 pub attestation_url: Option<String>,
42}
43
44#[derive(Debug, Serialize)]
45pub struct GetPubkeyRequest {
46 pub accessor: Value,
47 pub owner: String,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub profile: Option<String>,
50 pub secrets_json: String,
51}
52
53impl ApiClient {
54 pub fn new(network: &NetworkConfig) -> Self {
55 Self {
56 client: reqwest::Client::new(),
57 base_url: network.api_base_url.clone(),
58 }
59 }
60
61 pub async fn call_project(
63 &self,
64 owner: &str,
65 project: &str,
66 payment_key: &str,
67 body: &HttpsCallRequest,
68 compute_limit: Option<u64>,
69 deposit: Option<&str>,
70 ) -> Result<HttpsCallResponse> {
71 let url = format!("{}/call/{}/{}", self.base_url, owner, project);
72
73 let mut req = self
74 .client
75 .post(&url)
76 .header("X-Payment-Key", payment_key)
77 .json(body);
78
79 if let Some(limit) = compute_limit {
80 req = req.header("X-Compute-Limit", limit.to_string());
81 }
82 if let Some(deposit) = deposit {
83 req = req.header("X-Attached-Deposit", deposit);
84 }
85
86 let response = req.send().await.context("Failed to call project")?;
87
88 if !response.status().is_success() {
89 let status = response.status();
90 let text = response.text().await.unwrap_or_default();
91 anyhow::bail!("API error ({status}): {text}");
92 }
93
94 response
95 .json()
96 .await
97 .context("Failed to parse call response")
98 }
99
100 pub async fn get_call_result(
102 &self,
103 call_id: &str,
104 payment_key: &str,
105 ) -> Result<HttpsCallResponse> {
106 let url = format!("{}/calls/{}", self.base_url, call_id);
107
108 let response = self
109 .client
110 .get(&url)
111 .header("X-Payment-Key", payment_key)
112 .send()
113 .await
114 .context("Failed to poll call status")?;
115
116 if !response.status().is_success() {
117 let status = response.status();
118 let text = response.text().await.unwrap_or_default();
119 anyhow::bail!("API error ({status}): {text}");
120 }
121
122 response.json().await.context("Failed to parse call result")
123 }
124
125 pub async fn get_payment_key_balance(
127 &self,
128 owner: &str,
129 nonce: u32,
130 ) -> Result<PaymentKeyBalanceResponse> {
131 let url = format!(
132 "{}/public/payment-keys/{}/{}/balance",
133 self.base_url, owner, nonce
134 );
135
136 let response = self.client.get(&url).send().await?;
137
138 if !response.status().is_success() {
139 let status = response.status();
140 let text = response.text().await.unwrap_or_default();
141 anyhow::bail!("Failed to get balance ({status}): {text}");
142 }
143
144 response
145 .json()
146 .await
147 .context("Failed to parse balance response")
148 }
149
150 pub async fn get_payment_key_usage(
152 &self,
153 owner: &str,
154 nonce: u32,
155 limit: i64,
156 offset: i64,
157 ) -> Result<PaymentKeyUsageResponse> {
158 let url = format!(
159 "{}/public/payment-keys/{}/{}/usage?limit={}&offset={}",
160 self.base_url, owner, nonce, limit, offset
161 );
162
163 let response = self.client.get(&url).send().await?;
164
165 if !response.status().is_success() {
166 let status = response.status();
167 let text = response.text().await.unwrap_or_default();
168 anyhow::bail!("Failed to get usage ({status}): {text}");
169 }
170
171 response
172 .json()
173 .await
174 .context("Failed to parse usage response")
175 }
176
177 pub async fn get_project_owner_earnings(
179 &self,
180 owner: &str,
181 ) -> Result<ProjectOwnerEarningsResponse> {
182 let url = format!("{}/public/project-earnings/{}", self.base_url, owner);
183
184 let response = self.client.get(&url).send().await?;
185
186 if !response.status().is_success() {
187 let status = response.status();
188 let text = response.text().await.unwrap_or_default();
189 anyhow::bail!("Failed to get earnings ({status}): {text}");
190 }
191
192 response
193 .json()
194 .await
195 .context("Failed to parse earnings response")
196 }
197
198 pub async fn get_earnings_history(
200 &self,
201 owner: &str,
202 source: Option<&str>,
203 limit: i64,
204 offset: i64,
205 ) -> Result<EarningsHistoryResponse> {
206 let mut url = format!(
207 "{}/public/project-earnings/{}/history?limit={}&offset={}",
208 self.base_url, owner, limit, offset
209 );
210 if let Some(source) = source {
211 url.push_str(&format!("&source={}", source));
212 }
213
214 let response = self.client.get(&url).send().await?;
215
216 if !response.status().is_success() {
217 let status = response.status();
218 let text = response.text().await.unwrap_or_default();
219 anyhow::bail!("Failed to get earnings history ({status}): {text}");
220 }
221
222 response
223 .json()
224 .await
225 .context("Failed to parse earnings history")
226 }
227
228 pub async fn add_generated_secret(
230 &self,
231 req: &Value,
232 ) -> Result<AddGeneratedSecretResponse> {
233 let url = format!("{}/secrets/add_generated_secret", self.base_url);
234
235 let response = self
236 .client
237 .post(&url)
238 .json(req)
239 .send()
240 .await
241 .context("Failed to call add_generated_secret")?;
242
243 if !response.status().is_success() {
244 let status = response.status();
245 let text = response.text().await.unwrap_or_default();
246 anyhow::bail!("Failed to generate secrets ({status}): {text}");
247 }
248
249 response
250 .json()
251 .await
252 .context("Failed to parse add_generated_secret response")
253 }
254
255 pub async fn update_user_secrets(
257 &self,
258 payload: &Value,
259 ) -> Result<UpdateUserSecretsResponse> {
260 let url = format!("{}/secrets/update_user_secrets", self.base_url);
261
262 let response = self
263 .client
264 .post(&url)
265 .json(payload)
266 .send()
267 .await
268 .context("Failed to call update_user_secrets")?;
269
270 if !response.status().is_success() {
271 let status = response.status();
272 let text = response.text().await.unwrap_or_default();
273 anyhow::bail!("Failed to update secrets ({status}): {text}");
274 }
275
276 response
277 .json()
278 .await
279 .context("Failed to parse update_user_secrets response")
280 }
281
282 pub async fn get_secrets_pubkey(&self, request: &GetPubkeyRequest) -> Result<String> {
284 let url = format!("{}/secrets/pubkey", self.base_url);
285
286 let response = self
287 .client
288 .post(&url)
289 .json(request)
290 .send()
291 .await
292 .context("Failed to get secrets pubkey")?;
293
294 if !response.status().is_success() {
295 let status = response.status();
296 let text = response.text().await.unwrap_or_default();
297 anyhow::bail!("Failed to get pubkey ({status}): {text}");
298 }
299
300 #[derive(Deserialize)]
301 struct PubkeyResponse {
302 pubkey: String,
303 }
304
305 let resp: PubkeyResponse = response
306 .json()
307 .await
308 .context("Failed to parse pubkey response")?;
309
310 Ok(resp.pubkey)
311 }
312
313 pub async fn create_payment_check(
317 &self,
318 api_key: &str,
319 token: &str,
320 amount: &str,
321 memo: Option<&str>,
322 expires_in: Option<u64>,
323 ) -> Result<PaymentCheckCreateResponse> {
324 let url = format!("{}/wallet/v1/payment-check/create", self.base_url);
325
326 let mut body = serde_json::json!({
327 "token": token,
328 "amount": amount,
329 });
330 if let Some(memo) = memo {
331 body["memo"] = serde_json::Value::String(memo.to_string());
332 }
333 if let Some(expires_in) = expires_in {
334 body["expires_in"] = serde_json::Value::Number(expires_in.into());
335 }
336
337 let response = self
338 .client
339 .post(&url)
340 .header("Authorization", format!("Bearer {}", api_key))
341 .json(&body)
342 .send()
343 .await
344 .context("Failed to create payment check")?;
345
346 if !response.status().is_success() {
347 let status = response.status();
348 let text = response.text().await.unwrap_or_default();
349 anyhow::bail!("Failed to create payment check ({status}): {text}");
350 }
351
352 response
353 .json()
354 .await
355 .context("Failed to parse create check response")
356 }
357
358 pub async fn batch_create_payment_checks(
360 &self,
361 api_key: &str,
362 checks: &[serde_json::Value],
363 ) -> Result<PaymentCheckBatchCreateResponse> {
364 let url = format!("{}/wallet/v1/payment-check/batch-create", self.base_url);
365
366 let response = self
367 .client
368 .post(&url)
369 .header("Authorization", format!("Bearer {}", api_key))
370 .json(&serde_json::json!({ "checks": checks }))
371 .send()
372 .await
373 .context("Failed to batch create payment checks")?;
374
375 if !response.status().is_success() {
376 let status = response.status();
377 let text = response.text().await.unwrap_or_default();
378 anyhow::bail!("Failed to batch create checks ({status}): {text}");
379 }
380
381 response
382 .json()
383 .await
384 .context("Failed to parse batch create response")
385 }
386
387 pub async fn claim_payment_check(
389 &self,
390 api_key: &str,
391 check_key: &str,
392 amount: Option<&str>,
393 ) -> Result<PaymentCheckClaimResponse> {
394 let url = format!("{}/wallet/v1/payment-check/claim", self.base_url);
395
396 let mut body = serde_json::json!({ "check_key": check_key });
397 if let Some(amount) = amount {
398 body["amount"] = serde_json::Value::String(amount.to_string());
399 }
400
401 let response = self
402 .client
403 .post(&url)
404 .header("Authorization", format!("Bearer {}", api_key))
405 .json(&body)
406 .send()
407 .await
408 .context("Failed to claim payment check")?;
409
410 if !response.status().is_success() {
411 let status = response.status();
412 let text = response.text().await.unwrap_or_default();
413 anyhow::bail!("Failed to claim check ({status}): {text}");
414 }
415
416 response
417 .json()
418 .await
419 .context("Failed to parse claim response")
420 }
421
422 pub async fn reclaim_payment_check(
424 &self,
425 api_key: &str,
426 check_id: &str,
427 amount: Option<&str>,
428 ) -> Result<PaymentCheckReclaimResponse> {
429 let url = format!("{}/wallet/v1/payment-check/reclaim", self.base_url);
430
431 let mut body = serde_json::json!({ "check_id": check_id });
432 if let Some(amount) = amount {
433 body["amount"] = serde_json::Value::String(amount.to_string());
434 }
435
436 let response = self
437 .client
438 .post(&url)
439 .header("Authorization", format!("Bearer {}", api_key))
440 .json(&body)
441 .send()
442 .await
443 .context("Failed to reclaim payment check")?;
444
445 if !response.status().is_success() {
446 let status = response.status();
447 let text = response.text().await.unwrap_or_default();
448 anyhow::bail!("Failed to reclaim check ({status}): {text}");
449 }
450
451 response
452 .json()
453 .await
454 .context("Failed to parse reclaim response")
455 }
456
457 pub async fn get_payment_check_status(
459 &self,
460 api_key: &str,
461 check_id: &str,
462 ) -> Result<PaymentCheckStatusResponse> {
463 let url = format!(
464 "{}/wallet/v1/payment-check/status?check_id={}",
465 self.base_url, check_id
466 );
467
468 let response = self
469 .client
470 .get(&url)
471 .header("Authorization", format!("Bearer {}", api_key))
472 .send()
473 .await
474 .context("Failed to get check status")?;
475
476 if !response.status().is_success() {
477 let status = response.status();
478 let text = response.text().await.unwrap_or_default();
479 anyhow::bail!("Failed to get check status ({status}): {text}");
480 }
481
482 response
483 .json()
484 .await
485 .context("Failed to parse check status response")
486 }
487
488 pub async fn list_payment_checks(
490 &self,
491 api_key: &str,
492 status_filter: Option<&str>,
493 limit: i64,
494 ) -> Result<PaymentCheckListResponse> {
495 let mut url = format!(
496 "{}/wallet/v1/payment-check/list?limit={}",
497 self.base_url, limit
498 );
499 if let Some(status) = status_filter {
500 url.push_str(&format!("&status={}", status));
501 }
502
503 let response = self
504 .client
505 .get(&url)
506 .header("Authorization", format!("Bearer {}", api_key))
507 .send()
508 .await
509 .context("Failed to list payment checks")?;
510
511 if !response.status().is_success() {
512 let status = response.status();
513 let text = response.text().await.unwrap_or_default();
514 anyhow::bail!("Failed to list checks ({status}): {text}");
515 }
516
517 response
518 .json()
519 .await
520 .context("Failed to parse check list response")
521 }
522
523 pub async fn sign_message(
525 &self,
526 api_key: &str,
527 message: &str,
528 recipient: &str,
529 nonce: Option<&str>,
530 ) -> Result<SignMessageResponse> {
531 let url = format!("{}/wallet/v1/sign-message", self.base_url);
532
533 let mut body = serde_json::json!({
534 "message": message,
535 "recipient": recipient,
536 });
537 if let Some(nonce) = nonce {
538 body["nonce"] = serde_json::Value::String(nonce.to_string());
539 }
540
541 let response = self
542 .client
543 .post(&url)
544 .header("Authorization", format!("Bearer {}", api_key))
545 .json(&body)
546 .send()
547 .await
548 .context("Failed to sign message")?;
549
550 if !response.status().is_success() {
551 let status = response.status();
552 let text = response.text().await.unwrap_or_default();
553 anyhow::bail!("Failed to sign message ({status}): {text}");
554 }
555
556 response
557 .json()
558 .await
559 .context("Failed to parse sign message response")
560 }
561
562 pub async fn wallet_call(
564 &self,
565 wallet_key: &str,
566 receiver_id: &str,
567 method_name: &str,
568 args: serde_json::Value,
569 gas: u64,
570 deposit: u128,
571 ) -> Result<WalletCallResponse> {
572 let url = format!("{}/wallet/v1/call", self.base_url);
573
574 let body = serde_json::json!({
575 "receiver_id": receiver_id,
576 "method_name": method_name,
577 "args": args,
578 "gas": gas.to_string(),
579 "deposit": deposit.to_string(),
580 });
581
582 let response = self
583 .client
584 .post(&url)
585 .header("Authorization", format!("Bearer {}", wallet_key))
586 .json(&body)
587 .send()
588 .await
589 .context("Failed to call wallet API")?;
590
591 if !response.status().is_success() {
592 let status = response.status();
593 let text = response.text().await.unwrap_or_default();
594 anyhow::bail!("Wallet call failed ({status}): {text}");
595 }
596
597 response
598 .json()
599 .await
600 .context("Failed to parse wallet call response")
601 }
602
603 pub async fn wallet_call_raw(
605 &self,
606 wallet_key: &str,
607 receiver_id: &str,
608 method_name: &str,
609 args_raw: &[u8],
610 gas: u64,
611 deposit: u128,
612 ) -> Result<WalletCallResponse> {
613 let url = format!("{}/wallet/v1/call", self.base_url);
614
615 use base64::Engine;
616 let args_b64 = base64::engine::general_purpose::STANDARD.encode(args_raw);
617
618 let body = serde_json::json!({
619 "receiver_id": receiver_id,
620 "method_name": method_name,
621 "args_base64": args_b64,
622 "gas": gas.to_string(),
623 "deposit": deposit.to_string(),
624 });
625
626 let response = self
627 .client
628 .post(&url)
629 .header("Authorization", format!("Bearer {}", wallet_key))
630 .json(&body)
631 .send()
632 .await
633 .context("Failed to call wallet API")?;
634
635 if !response.status().is_success() {
636 let status = response.status();
637 let text = response.text().await.unwrap_or_default();
638 anyhow::bail!("Wallet call failed ({status}): {text}");
639 }
640
641 response
642 .json()
643 .await
644 .context("Failed to parse wallet call response")
645 }
646
647 pub async fn peek_payment_check(
649 &self,
650 api_key: &str,
651 check_key: &str,
652 ) -> Result<PaymentCheckPeekResponse> {
653 let url = format!("{}/wallet/v1/payment-check/peek", self.base_url);
654
655 let response = self
656 .client
657 .post(&url)
658 .header("Authorization", format!("Bearer {}", api_key))
659 .json(&serde_json::json!({ "check_key": check_key }))
660 .send()
661 .await
662 .context("Failed to peek payment check")?;
663
664 if !response.status().is_success() {
665 let status = response.status();
666 let text = response.text().await.unwrap_or_default();
667 anyhow::bail!("Failed to peek check ({status}): {text}");
668 }
669
670 response
671 .json()
672 .await
673 .context("Failed to parse peek response")
674 }
675}
676
677#[derive(Debug, Deserialize)]
680#[allow(dead_code)]
681pub struct PaymentKeyBalanceResponse {
682 pub owner: String,
683 pub nonce: u32,
684 pub initial_balance: String,
685 pub spent: String,
686 pub reserved: String,
687 pub available: String,
688 pub last_used_at: Option<String>,
689}
690
691#[derive(Debug, Deserialize)]
692pub struct PaymentKeyUsageResponse {
693 pub usage: Vec<PaymentKeyUsageItem>,
694 pub total: i64,
695}
696
697#[derive(Debug, Deserialize)]
698#[allow(dead_code)]
699pub struct PaymentKeyUsageItem {
700 pub call_id: String,
701 pub project_id: String,
702 pub compute_cost: String,
703 pub attached_deposit: String,
704 pub status: String,
705 pub created_at: String,
706}
707
708#[derive(Debug, Deserialize)]
709#[allow(dead_code)]
710pub struct ProjectOwnerEarningsResponse {
711 pub project_owner: String,
712 pub balance: String,
713 pub total_earned: String,
714}
715
716#[derive(Debug, Deserialize)]
717pub struct EarningsHistoryResponse {
718 pub earnings: Vec<EarningRecord>,
719 pub total_count: i64,
720}
721
722#[derive(Debug, Deserialize)]
723pub struct EarningRecord {
724 pub project_id: String,
725 pub amount: String,
726 pub source: String,
727 pub created_at: i64,
728}
729
730#[derive(Debug, Deserialize)]
731pub struct AddGeneratedSecretResponse {
732 pub encrypted_data_base64: String,
733 #[allow(dead_code)]
734 pub all_keys: Vec<String>,
735}
736
737#[derive(Debug, Deserialize)]
738pub struct UpdateUserSecretsResponse {
739 pub encrypted_secrets_base64: String,
740}
741
742#[derive(Debug, Deserialize)]
745#[allow(dead_code)]
746pub struct SignMessageResponse {
747 pub account_id: String,
748 pub signature: String,
750 pub signature_base64: String,
752 pub public_key: String,
753 pub nonce: String,
754}
755
756#[derive(Debug, Deserialize)]
759#[allow(dead_code)]
760pub struct WalletCallResponse {
761 pub request_id: String,
762 pub status: String,
763 pub tx_hash: Option<String>,
764 pub result: Option<serde_json::Value>,
765 pub approval_id: Option<String>,
766}
767
768#[derive(Debug, Deserialize)]
771#[allow(dead_code)]
772pub struct PaymentCheckCreateResponse {
773 pub check_id: String,
774 pub check_key: String,
775 pub token: String,
776 pub amount: String,
777 pub memo: Option<String>,
778 pub created_at: String,
779 pub expires_at: Option<String>,
780}
781
782#[derive(Debug, Deserialize)]
783pub struct PaymentCheckBatchCreateResponse {
784 pub checks: Vec<PaymentCheckCreateResponse>,
785}
786
787#[derive(Debug, Deserialize)]
788#[allow(dead_code)]
789pub struct PaymentCheckClaimResponse {
790 pub token: String,
791 pub amount_claimed: String,
792 pub remaining: String,
793 pub memo: Option<String>,
794 pub claimed_at: String,
795 pub intent_hash: Option<String>,
796}
797
798#[derive(Debug, Deserialize)]
799#[allow(dead_code)]
800pub struct PaymentCheckReclaimResponse {
801 pub token: String,
802 pub amount_reclaimed: String,
803 pub remaining: String,
804 pub reclaimed_at: String,
805 pub intent_hash: Option<String>,
806}
807
808#[derive(Debug, Deserialize)]
809#[allow(dead_code)]
810pub struct PaymentCheckStatusResponse {
811 pub check_id: String,
812 pub token: String,
813 pub amount: String,
814 pub claimed_amount: String,
815 pub reclaimed_amount: String,
816 pub status: String,
817 pub memo: Option<String>,
818 pub created_at: String,
819 pub expires_at: Option<String>,
820 pub claimed_at: Option<String>,
821 pub claimed_by: Option<String>,
822}
823
824#[derive(Debug, Deserialize)]
825pub struct PaymentCheckListResponse {
826 pub checks: Vec<PaymentCheckStatusResponse>,
827}
828
829#[derive(Debug, Deserialize)]
830#[allow(dead_code)]
831pub struct PaymentCheckPeekResponse {
832 pub token: String,
833 pub balance: String,
834 pub memo: Option<String>,
835 pub status: String,
836 pub expires_at: Option<String>,
837}