mercury_cli/
lib.rs

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        //        force_replace: bool,
211        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![], // should be unreachable anyways
371        };
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}