1use crate::models::*;
2use base64ct::{Base64, Encoding};
3use sui_crypto::ed25519::Ed25519PrivateKey;
4use sui_crypto::SuiSigner;
5use sui_sdk_types::{Address, PersonalMessage};
6
7fn 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#[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#[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
131fn 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
161pub 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}