Skip to main content

zing_cli/
api.rs

1use crate::models::*;
2use base64ct::{Base64, Encoding};
3use sui_crypto::ed25519::Ed25519PrivateKey;
4use sui_crypto::SuiSigner;
5use sui_sdk_types::{Address, PersonalMessage};
6
7/// Sign the BCS-encoded ApiAccessMessage as a PersonalMessage.
8/// Returns (signature_base64, bytes_base64).
9fn sign_access_message(
10    keypair: &Ed25519PrivateKey,
11    q: &str,
12    wiki: &str,
13    transaction_digest: &str,
14    expand: Option<bool>,
15    article_ids: Option<Vec<String>>,
16) -> anyhow::Result<(String, String)> {
17    let timestamp = std::time::SystemTime::now()
18        .duration_since(std::time::UNIX_EPOCH)
19        .unwrap()
20        .as_millis() as u64;
21
22    let msg = ApiAccessMessage {
23        q: q.to_string(),
24        wiki: wiki.to_string(),
25        transaction_digest: transaction_digest.to_string(),
26        timestamp,
27        expand,
28        article_ids,
29    };
30
31    let bcs_bytes = bcs::to_bytes(&msg)?;
32    let bytes_b64 = Base64::encode_string(&bcs_bytes);
33
34    let signature = keypair
35        .sign_personal_message(&PersonalMessage(bcs_bytes.clone().into()))
36        .map_err(|e| anyhow::anyhow!("Signing ApiAccessMessage failed: {e}"))?;
37
38    let sig_b64 = signature.to_base64();
39
40    Ok((sig_b64, bytes_b64))
41}
42
43/// POST to the search endpoint. Returns the response.
44#[allow(clippy::too_many_arguments)]
45pub async fn search(
46    rpc_url: &str,
47    api_base_url: &str,
48    keypair: &Ed25519PrivateKey,
49    sender: &Address,
50    platform_usdc_address: &Address,
51    q: &str,
52    wiki: &str,
53    owner: Option<&str>,
54    limit: u32,
55) -> anyhow::Result<SearchResponse> {
56    let tx_digest = crate::sui::send_payment(rpc_url, keypair, sender, platform_usdc_address).await?;
57
58    let (signature, bytes) = sign_access_message(keypair, q, wiki, &tx_digest, None, None)?;
59
60    let body = PaidRequest {
61        q: q.to_string(),
62        wiki: wiki.to_string(),
63        owner: owner.map(|s| s.to_string()),
64        limit,
65        expand: None,
66        article_ids: None,
67        transaction_digest: tx_digest,
68        signature,
69        bytes,
70    };
71
72    let client = reqwest::Client::new();
73    let url = format!("{}/search", api_base_url.trim_end_matches('/'));
74    let resp = client.post(&url).json(&body).send().await?;
75
76    let status = resp.status();
77    if !status.is_success() {
78        let body_text = resp.text().await.unwrap_or_default();
79        anyhow::bail!("API error ({}): {}", status.as_u16(), body_text);
80    }
81
82    let search_resp: SearchResponse = resp.json().await?;
83    Ok(search_resp)
84}
85
86/// POST to the chunks endpoint. Returns the response.
87#[allow(clippy::too_many_arguments)]
88pub async fn chunks(
89    rpc_url: &str,
90    api_base_url: &str,
91    keypair: &Ed25519PrivateKey,
92    sender: &Address,
93    platform_usdc_address: &Address,
94    q: &str,
95    wiki: &str,
96    owner: Option<&str>,
97    limit: u32,
98    expand: Option<bool>,
99    article_ids: Option<Vec<String>>,
100) -> anyhow::Result<ChunksResponse> {
101    let tx_digest = crate::sui::send_payment(rpc_url, keypair, sender, platform_usdc_address).await?;
102
103    let (signature, bytes) = sign_access_message(keypair, q, wiki, &tx_digest, expand, article_ids.clone())?;
104
105    let body = PaidRequest {
106        q: q.to_string(),
107        wiki: wiki.to_string(),
108        owner: owner.map(|s| s.to_string()),
109        limit,
110        expand,
111        article_ids,
112        transaction_digest: tx_digest,
113        signature,
114        bytes,
115    };
116
117    let client = reqwest::Client::new();
118    let url = format!("{}/chunks", api_base_url.trim_end_matches('/'));
119    let resp = client.post(&url).json(&body).send().await?;
120
121    let status = resp.status();
122    if !status.is_success() {
123        let body_text = resp.text().await.unwrap_or_default();
124        anyhow::bail!("API error ({}): {}", status.as_u16(), body_text);
125    }
126
127    let chunks_resp: ChunksResponse = resp.json().await?;
128    Ok(chunks_resp)
129}
130
131/// Sign the BCS-encoded ExpandAccessMessage as a PersonalMessage.
132/// Returns (signature_base64, bytes_base64).
133fn sign_expand_message(
134    keypair: &Ed25519PrivateKey,
135    chunk_ids: &[i64],
136    transaction_digest: &str,
137) -> anyhow::Result<(String, String)> {
138    let timestamp = std::time::SystemTime::now()
139        .duration_since(std::time::UNIX_EPOCH)
140        .unwrap()
141        .as_millis() as u64;
142
143    let msg = ExpandAccessMessage {
144        chunk_ids: chunk_ids.to_vec(),
145        transaction_digest: transaction_digest.to_string(),
146        timestamp,
147    };
148
149    let bcs_bytes = bcs::to_bytes(&msg)?;
150    let bytes_b64 = Base64::encode_string(&bcs_bytes);
151
152    let signature = keypair
153        .sign_personal_message(&PersonalMessage(bcs_bytes.clone().into()))
154        .map_err(|e| anyhow::anyhow!("Signing ExpandAccessMessage failed: {e}"))?;
155
156    let sig_b64 = signature.to_base64();
157
158    Ok((sig_b64, bytes_b64))
159}
160
161/// POST to the chunk/expand endpoint. Returns the full untruncated text for given chunks.
162pub async fn expand_chunks(
163    rpc_url: &str,
164    api_base_url: &str,
165    keypair: &Ed25519PrivateKey,
166    sender: &Address,
167    platform_usdc_address: &Address,
168    chunk_ids: &[i64],
169) -> anyhow::Result<ExpandResponse> {
170    let tx_digest = crate::sui::send_payment(rpc_url, keypair, sender, platform_usdc_address).await?;
171
172    let (signature, bytes) = sign_expand_message(keypair, chunk_ids, &tx_digest)?;
173
174    let body = ExpandRequest {
175        chunk_ids: chunk_ids.to_vec(),
176        transaction_digest: tx_digest,
177        signature,
178        bytes,
179    };
180
181    let client = reqwest::Client::new();
182    let url = format!("{}/chunk/expand", api_base_url.trim_end_matches('/'));
183    let resp = client.post(&url).json(&body).send().await?;
184
185    let status = resp.status();
186    if !status.is_success() {
187        let body_text = resp.text().await.unwrap_or_default();
188        anyhow::bail!("API error ({}): {}", status.as_u16(), body_text);
189    }
190
191    let expand_resp: ExpandResponse = resp.json().await?;
192    Ok(expand_resp)
193}