1use parser::{Column, Table};
2use serde::{Deserialize, Serialize};
3
4use std::fs::File;
5use std::io::Read;
6
7use clap::{Parser, Subcommand};
8
9mod error;
10mod parser;
11mod specification;
12
13pub use parser::ZephyrProjectParser;
14
15#[derive(Parser)]
16#[command(author, version, about, long_about = None)]
17pub struct Cli {
18 #[arg(short, long)]
19 pub jwt: Option<String>,
20
21 #[arg(short, long)]
22 pub key: Option<String>,
23
24 #[arg(short, long)]
25 pub local: Option<bool>,
26
27 #[arg(short, long)]
28 pub mainnet: Option<bool>,
29
30 #[command(subcommand)]
31 pub command: Option<Commands>,
32}
33
34#[derive(Subcommand)]
35pub enum Commands {
36 Deploy {
37 #[arg(short, long)]
38 target: Option<String>,
39
40 #[arg(short, long)]
41 old_api: Option<bool>,
42
43 #[arg(short, long)]
44 force: Option<bool>,
45 },
46
47 Retroshade {
48 #[arg(short, long)]
49 target: String,
50
51 #[arg(short, long)]
52 project: String,
53
54 #[arg(short, long)]
55 contracts: Vec<String>,
56 },
57
58 Build,
59
60 Catchup {
61 #[arg(short, long)]
62 retroshades: Option<bool>,
63
64 #[arg(short, long)]
65 functions: Option<Vec<String>>,
66
67 #[arg(short, long)]
68 project_name: Option<String>,
69
70 #[arg(short, long)]
71 contracts: Vec<String>,
72
73 #[arg(short, long)]
74 start: Option<i64>,
75
76 #[arg(short, long)]
77 topic1s: Option<Vec<String>>,
78
79 #[arg(short, long)]
80 topic2s: Option<Vec<String>>,
81
82 #[arg(short, long)]
83 topic3s: Option<Vec<String>>,
84
85 #[arg(short, long)]
86 topic4s: Option<Vec<String>>,
87 },
88
89 NewProject {
90 #[arg(short, long)]
91 name: String,
92 },
93}
94
95#[derive(Deserialize, Serialize, Debug)]
96struct NewZephyrTableClient {
97 table: Option<String>,
98 columns: Option<Vec<Column>>,
99 force: bool,
100}
101
102#[derive(Deserialize, Serialize, Debug)]
103struct CodeUploadClient {
104 code: Option<Vec<u8>>,
105 force_replace: Option<bool>,
106 project_name: Option<String>,
107 contract: Option<bool>,
108 contracts: Option<Vec<String>>,
109}
110
111pub enum MercuryAccessKey {
112 Jwt(String),
113 Key(String),
114 None,
115}
116
117impl MercuryAccessKey {
118 pub fn from_jwt(jwt: &str) -> Self {
119 Self::Jwt(jwt.to_string())
120 }
121
122 pub fn from_key(key: &str) -> Self {
123 Self::Key(key.to_string())
124 }
125}
126
127pub struct MercuryClient {
128 pub base_url: String,
129 pub key: MercuryAccessKey,
130}
131
132impl MercuryClient {
133 pub fn new(base_url: String, key: MercuryAccessKey) -> Self {
134 Self { base_url, key }
135 }
136
137 pub fn get_auth(&self) -> String {
138 match &self.key {
139 MercuryAccessKey::Jwt(jwt) => format!("Bearer {}", &jwt),
140 MercuryAccessKey::Key(key) => key.to_string(),
141 MercuryAccessKey::None => panic!("Invalid CLI configuration. Set auth required."),
142 }
143 }
144
145 pub async fn new_table(
146 &self,
147 table: Table,
148 force: bool,
149 ) -> Result<(), Box<dyn std::error::Error>> {
150 let columns = table.columns;
151 let mut cols = Vec::new();
152
153 for col in columns {
154 cols.push(Column {
155 name: col.name.to_string(),
156 col_type: col.col_type.to_string(),
157 primary: col.primary.clone(),
158 index: col.index.clone(),
159 });
160 }
161
162 let code = NewZephyrTableClient {
163 table: Some(table.name),
164 columns: Some(cols),
165 force: if force {
166 force
167 } else {
168 if let Some(force) = table.force {
169 force
170 } else {
171 false
172 }
173 },
174 };
175
176 let json_code = serde_json::to_string(&code)?;
177 let url = format!("{}/zephyr_table_new", &self.base_url);
178 let authorization = self.get_auth();
179
180 let client = reqwest::Client::new();
181
182 let response = client
183 .post(url)
184 .header("Content-Type", "application/json")
185 .header("Authorization", authorization)
186 .body(json_code)
187 .send()
188 .await
189 .unwrap();
190
191 if response.status().is_success() {
192 println!(
193 "[+] Table \"{}\" created successfully",
194 response.text().await.unwrap()
195 );
196 } else {
197 println!(
198 "[-] Request failed with status code: {:?}, Error: {}",
199 response.status(),
200 response.text().await.unwrap()
201 );
202 };
203
204 Ok(())
205 }
206
207 pub async fn deploy(
208 &self,
209 wasm: String,
210 project_name: Option<String>,
212 contracts: Option<Vec<String>>,
213 ) -> Result<(), Box<dyn std::error::Error>> {
214 println!("Reading wasm {}", wasm);
215 let mut input_file = File::open(wasm)?;
216
217 let mut buffer = Vec::new();
218 input_file.read_to_end(&mut buffer)?;
219 println!(
220 "(Size of program is {}, wasm hash: {})",
221 buffer.len(),
222 hex::encode(md5::compute(&buffer).0)
223 );
224
225 let code = CodeUploadClient {
226 code: Some(buffer),
227 force_replace: Some(true),
228 project_name,
229 contract: if contracts.is_some() {
230 Some(true)
231 } else {
232 None
233 },
234 contracts,
235 };
236 let json_code = serde_json::to_string(&code)?;
237
238 let url = format!("{}/zephyr_upload", &self.base_url);
239 let authorization = self.get_auth();
240
241 let client = reqwest::Client::new();
242
243 let response = client
244 .post(url)
245 .header("Content-Type", "application/json")
246 .header("Authorization", authorization)
247 .body(json_code)
248 .send()
249 .await
250 .unwrap();
251
252 if response.status().is_success() {
253 println!("[+] Deployed was successful!");
254 } else {
255 println!(
256 "[-] Request failed with status code: {:?}",
257 response.status()
258 );
259 };
260
261 Ok(())
262 }
263
264 pub async fn catchup_standard(
265 &self,
266 contracts: Vec<String>,
267 project_name: Option<String>,
268 ) -> Result<(), Box<dyn std::error::Error>> {
269 let request = CatchupRequest {
270 mode: ExecutionMode::EventCatchup(contracts),
271 project_name,
272 };
273
274 self.catchup(request, false).await?;
275
276 Ok(())
277 }
278
279 pub async fn retroshades_catchup(
280 &self,
281 fnames: Option<Vec<String>>,
282 start: Option<i64>,
283 project_name: Option<String>,
284 ) -> Result<(), Box<dyn std::error::Error>> {
285 let request = CatchupRequest {
286 mode: ExecutionMode::RetroshadesCatchup(RetroshadesCatchupMode {
287 fnames: fnames.unwrap_or(vec![]),
288 start_at: start.unwrap_or(0),
289 }),
290 project_name,
291 };
292
293 self.catchup(request, true).await?;
294 Ok(())
295 }
296
297 pub async fn catchup_scoped(
298 &self,
299 contracts: Vec<String>,
300 topic1s: Vec<String>,
301 topic2s: Vec<String>,
302 topic3s: Vec<String>,
303 topic4s: Vec<String>,
304 start: i64,
305 project_name: Option<String>,
306 ) -> Result<(), Box<dyn std::error::Error>> {
307 let request = CatchupRequest {
308 mode: ExecutionMode::EventCatchupScoped(ScopedEventCatchup {
309 contracts,
310 topic1s,
311 topic2s,
312 topic3s,
313 topic4s,
314 start,
315 }),
316 project_name,
317 };
318
319 self.catchup(request, false).await?;
320
321 Ok(())
322 }
323
324 async fn catchup(
325 &self,
326 request: CatchupRequest,
327 retroshades: bool,
328 ) -> Result<(), Box<dyn std::error::Error>> {
329 if !retroshades {
330 println!("Subscribing to the requested contracts.");
331 self.contracts_subscribe(request.mode.clone()).await;
332 }
333
334 let json_code = serde_json::to_string(&request)?;
335
336 let url = format!("{}/zephyr/execute", &self.base_url);
337 let authorization = self.get_auth();
338
339 let client = reqwest::Client::new();
340
341 let response = client
342 .post(url)
343 .header("Content-Type", "application/json")
344 .header("Authorization", authorization)
345 .body(json_code)
346 .send()
347 .await
348 .unwrap();
349
350 if response.status().is_success() {
351 println!(
352 "Catchup request sent successfully: {}",
353 response.text().await.unwrap()
354 )
355 } else {
356 println!(
357 "[-] Request failed with status code: {:?}, {}",
358 response.status(),
359 response.text().await.unwrap()
360 );
361 };
362
363 Ok(())
364 }
365
366 async fn contracts_subscribe(&self, mode: ExecutionMode) {
367 let contracts = match mode {
368 ExecutionMode::EventCatchup(contracts) => contracts,
369 ExecutionMode::EventCatchupScoped(ScopedEventCatchup { contracts, .. }) => contracts,
370 _ => vec![], };
372
373 let graphql_url = format!("{}/graphql", &self.base_url);
374 let authorization = self.get_auth();
375 let query = r#"
376 query {
377 allContractEventSubscriptions {
378 edges {
379 node {
380 contractId
381 }
382 }
383 }
384 }
385 "#;
386
387 let client = reqwest::Client::new();
388
389 let existing_subscriptions: Result<Vec<String>, _> = client
390 .post(&graphql_url)
391 .header("Authorization", &authorization)
392 .header("Content-Type", "application/json")
393 .json(&serde_json::json!({
394 "query": query,
395 }))
396 .send()
397 .await
398 .unwrap()
399 .json::<serde_json::Value>()
400 .await
401 .map(|json| {
402 json["data"]["allContractEventSubscriptions"]["edges"]
403 .as_array()
404 .map(|edges| {
405 edges
406 .iter()
407 .filter_map(|edge| {
408 edge["node"]["contractId"].as_str().map(String::from)
409 })
410 .collect()
411 })
412 .unwrap_or_default()
413 });
414
415 let existing_subscriptions = match existing_subscriptions {
416 Ok(subs) => subs,
417 Err(e) => {
418 println!("Error fetching existing subscriptions: {}", e);
419 vec![]
420 }
421 };
422
423 for contract in contracts {
424 if existing_subscriptions.contains(&contract) {
425 println!("Already subscribed to events for contract: {}", contract);
426 continue;
427 }
428
429 let url = format!("{}/event", &self.base_url);
430 let body = serde_json::json!({ "contract_id": contract });
431
432 match client
433 .post(&url)
434 .header("Authorization", &authorization)
435 .json(&body)
436 .send()
437 .await
438 {
439 Ok(response) => {
440 if response.status().is_success() {
441 println!(
442 "Successfully subscribed to events for contract: {}",
443 contract
444 );
445 } else {
446 println!(
447 "Failed to subscribe to events for contract: {}. Status: {:?}",
448 contract,
449 response.status()
450 );
451 }
452 }
453 Err(e) => println!(
454 "Error subscribing to events for contract {}: {}",
455 contract, e
456 ),
457 }
458 }
459 }
460}
461
462#[derive(Serialize, Deserialize, Clone, Debug)]
463pub struct InvokeZephyrFunction {
464 fname: String,
465 arguments: String,
466}
467
468#[derive(Serialize, Deserialize, Clone, Debug)]
469pub struct ScopedEventCatchup {
470 contracts: Vec<String>,
471 topic1s: Vec<String>,
472 topic2s: Vec<String>,
473 topic3s: Vec<String>,
474 topic4s: Vec<String>,
475 start: i64,
476}
477
478#[derive(Serialize, Deserialize, Clone, Debug)]
479pub enum ExecutionMode {
480 EventCatchup(Vec<String>),
481 EventCatchupScoped(ScopedEventCatchup),
482 Function(InvokeZephyrFunction),
483 RetroshadesCatchup(RetroshadesCatchupMode),
484}
485
486#[derive(Serialize, Deserialize, Clone, Debug)]
487pub struct RetroshadesCatchupMode {
488 pub fnames: Vec<String>,
489 pub start_at: i64,
490}
491
492#[derive(Serialize, Deserialize, Clone, Debug)]
493pub struct CatchupRequest {
494 mode: ExecutionMode,
495 project_name: Option<String>,
496}