znap_cli/utils/
mod.rs

1use crate::template;
2use serde::{Deserialize, Serialize};
3use solana_sdk::signature::Keypair;
4use std::collections::HashMap;
5use std::fs::{copy, create_dir, create_dir_all, read_dir, remove_dir_all};
6use std::io::Write;
7use std::process::{Child, Stdio};
8use std::{
9    fs::{read_to_string, File},
10    path::{Path, PathBuf},
11};
12
13#[derive(Serialize, Deserialize, Debug)]
14pub struct Status {
15    pub active: bool,
16}
17
18#[derive(Serialize, Deserialize, Debug)]
19pub struct Collection {
20    pub name: String,
21    pub address: String,
22    pub port: u16,
23    pub protocol: String,
24}
25
26#[derive(Serialize, Deserialize, Debug)]
27pub struct Config {
28    pub collections: Option<Vec<Collection>>,
29    pub identity: Option<String>,
30    pub rpc_url: Option<String>,
31}
32
33pub fn get_cwd() -> PathBuf {
34    std::env::current_dir().expect("Shoud be able to read cwd")
35}
36
37pub fn get_config() -> Config {
38    let cwd = get_cwd();
39    let znap_file_path = cwd.join("Znap.toml");
40    let znap_file = read_to_string(znap_file_path)
41        .expect("Should be able to read Znap.toml file. Make sure you are in a Znap workspace.");
42
43    toml::from_str(&znap_file).expect("Znap.toml file should have the proper format")
44}
45
46fn get_identity(identity: &str) -> String {
47    shellexpand::tilde(identity).into()
48}
49
50pub fn write_file(path: &Path, content: &str) {
51    let mut file = File::create(path).expect("Should be able to open file");
52    file.write_all(content.as_bytes())
53        .unwrap_or_else(|_| panic!("Should be able to write file: {path:?}"));
54}
55
56pub fn get_envs(
57    config: &Config,
58    collection: &Collection,
59    address: Option<&str>,
60    port: Option<&u16>,
61    protocol: Option<&str>,
62) -> HashMap<&'static str, String> {
63    let mut env_vars: HashMap<&str, String> = HashMap::new();
64
65    if let Ok(path) = std::env::var("IDENTITY_KEYPAIR_PATH") {
66        env_vars.insert("IDENTITY_KEYPAIR_PATH", get_identity(&path));
67    } else if let Ok(v) = std::env::var("IDENTITY_KEYPAIR") {
68        env_vars.insert("IDENTITY_KEYPAIR", v);
69    } else if let Some(i) = config.identity.as_deref() {
70        env_vars.insert("IDENTITY_KEYPAIR_PATH", get_identity(i));
71    }
72
73    if let Some(rpc_url) = &config.rpc_url {
74        env_vars.insert("RPC_URL", rpc_url.to_string());
75    }
76
77    if let Some(address) = address.or(Some(&collection.address)) {
78        env_vars.insert("COLLECTION_ADDRESS", address.to_owned());
79    }
80
81    if let Some(port) = port.or(Some(&collection.port)) {
82        env_vars.insert("COLLECTION_PORT", port.to_string());
83    }
84
85    if let Some(protocol) = protocol.or(Some(&collection.protocol)) {
86        env_vars.insert("COLLECTION_PROTOCOL", protocol.to_owned());
87    }
88
89    env_vars
90}
91
92pub fn start_server_blocking(
93    config: &Config,
94    collection: &Collection,
95    address: Option<&str>,
96    port: Option<&u16>,
97    protocol: Option<&str>,
98) {
99    let start_server_process = start_server(config, collection, address, port, protocol);
100    let exit = start_server_process
101        .wait_with_output()
102        .expect("Should be able to start server");
103
104    if !exit.status.success() {
105        std::process::exit(exit.status.code().unwrap_or(1));
106    }
107}
108
109pub fn start_server(
110    config: &Config,
111    collection: &Collection,
112    address: Option<&str>,
113    port: Option<&u16>,
114    protocol: Option<&str>,
115) -> Child {
116    std::process::Command::new("cargo")
117        .envs(get_envs(config, collection, address, port, protocol))
118        .arg("run")
119        .arg("--manifest-path")
120        .arg(get_cwd().join(format!(".znap/collections/{}/Cargo.toml", collection.name)))
121        .arg("--bin")
122        .arg("serve")
123        .stdout(Stdio::inherit())
124        .stderr(Stdio::inherit())
125        .spawn()
126        .map_err(|e| anyhow::format_err!("{}", e.to_string()))
127        .expect("Should be able to start server")
128}
129
130pub fn run_test_suite() {
131    let output = std::process::Command::new("npm")
132        .arg("run")
133        .arg("test")
134        .stdout(Stdio::inherit())
135        .stderr(Stdio::inherit())
136        .output()
137        .expect("Should wait until the tests are over");
138
139    if !output.status.success() {
140        panic!("Test failed: {}", String::from_utf8_lossy(&output.stdout));
141    }
142}
143
144pub fn wait_for_server(address: &str, port: &u16, protocol: &str) {
145    let url = format!("{protocol}://{address}:{port}/status");
146
147    loop {
148        if let Ok(response) = reqwest::blocking::get(&url) {
149            if let Ok(status) = response.json::<Status>() {
150                if status.active {
151                    break;
152                }
153            }
154        }
155
156        std::thread::sleep(std::time::Duration::from_millis(1000));
157    }
158}
159
160pub fn deploy_to_shuttle(project: &str, collection: &Collection) {
161    std::process::Command::new("cargo")
162        .arg("shuttle")
163        .arg("deploy")
164        .arg("--allow-dirty")
165        .arg("--name")
166        .arg(project)
167        .arg("--working-directory")
168        .arg(get_cwd().join(format!(".znap/collections/{}", collection.name)))
169        .stdout(Stdio::inherit())
170        .stderr(Stdio::inherit())
171        .output()
172        .expect("Should wait until the deploy is over");
173}
174
175pub fn copy_recursively(source: impl AsRef<Path>, destination: impl AsRef<Path>) {
176    create_dir_all(&destination).unwrap();
177    for entry in read_dir(source).unwrap() {
178        let entry = entry.unwrap();
179        let filetype = entry.file_type().unwrap();
180        if filetype.is_dir() {
181            copy_recursively(entry.path(), destination.as_ref().join(entry.file_name()));
182        } else {
183            copy(entry.path(), destination.as_ref().join(entry.file_name())).unwrap();
184        }
185    }
186}
187
188pub fn get_identity_keypair(config: &Config, collection: &Collection) -> Keypair {
189    let envs = get_envs(config, collection, None, None, None);
190
191    match envs.get("IDENTITY_KEYPAIR") {
192        Some(keypair) => Keypair::from_base58_string(keypair),
193        _ => match envs.get("IDENTITY_KEYPAIR_PATH") {
194            Some(path) => {
195                let keypair_file = std::fs::read_to_string(path).unwrap();
196                let keypair_bytes = keypair_file
197                    .trim_start_matches('[')
198                    .trim_end_matches(']')
199                    .split(',')
200                    .map(|b| b.trim().parse::<u8>().unwrap())
201                    .collect::<Vec<_>>();
202
203                Keypair::from_bytes(&keypair_bytes).unwrap()
204            }
205            _ => panic!("Identity not valid."),
206        },
207    }
208}
209
210pub fn generate_collection_executable_files(config: &Config, collection: &Collection) {
211    let cwd = get_cwd();
212    let znap_path = cwd.join(".znap");
213
214    if !znap_path.exists() {
215        create_dir(&znap_path).expect("Could not create .znap folder");
216    }
217
218    let znap_toml_path = znap_path.join("Cargo.toml");
219
220    write_file(
221        &znap_toml_path,
222        "[workspace]\nmembers = [\"collections/*\"]\nresolver = \"2\"\n\n[patch.crates-io]\ncurve25519-dalek = { git = \"https://github.com/dalek-cryptography/curve25519-dalek\", rev = \"8274d5cbb6fc3f38cdc742b4798173895cd2a290\" }",
223    );
224
225    let znap_collections_path = znap_path.join("collections");
226
227    if !znap_collections_path.exists() {
228        create_dir(&znap_collections_path).expect("Could not create .znap/collections folder");
229    }
230
231    let znap_collection_path = znap_collections_path.join(&collection.name);
232
233    if znap_collection_path.exists() {
234        remove_dir_all(&znap_collection_path)
235            .unwrap_or_else(|_| panic!("Could not delete .znap/{} folder", &collection.name))
236    }
237
238    create_dir(&znap_collection_path)
239        .unwrap_or_else(|_| panic!("Could not create .znap/{} folder", &collection.name));
240
241    let identity_keypair = get_identity_keypair(config, collection);
242    let rpc_url = config
243        .rpc_url
244        .as_ref()
245        .unwrap_or_else(|| panic!("RPC url is not defined"));
246    let secrets_content = format!(
247        "IDENTITY_KEYPAIR=\"{}\"\nRPC_URL=\"{}\"",
248        identity_keypair.to_base58_string(),
249        rpc_url,
250    );
251    let secrets_path = znap_collection_path.join("Secrets.toml");
252
253    write_file(&secrets_path, &secrets_content);
254
255    let znap_collection_src_path = znap_collection_path.join("src");
256
257    create_dir(&znap_collection_src_path)
258        .unwrap_or_else(|_| panic!("Could not create .znap/{}/src folder", &collection.name));
259
260    let znap_collection_src_bin_path = znap_collection_src_path.join("bin");
261
262    create_dir(&znap_collection_src_bin_path)
263        .unwrap_or_else(|_| panic!("Could not create .znap/{}/src/bin folder", &collection.name));
264
265    let collection_path = cwd.join(format!("collections/{}", &collection.name));
266    let collection_src_path = collection_path.join("src");
267
268    copy_recursively(collection_src_path, znap_collection_src_path);
269
270    // Generate the binaries
271    let znap_collection_src_bin_serve_path = znap_collection_src_bin_path.join("serve.rs");
272    write_file(
273        &znap_collection_src_bin_serve_path,
274        &template::collection_serve_binary::template(collection),
275    );
276
277    let znap_collection_src_bin_deploy_path = znap_collection_src_bin_path.join("deploy.rs");
278    write_file(
279        &znap_collection_src_bin_deploy_path,
280        &template::collection_deploy_binary::template(&collection.name),
281    );
282
283    // Generate a toml with collection and extras for serve/deploy
284    let znap_collection_toml_path = znap_collection_path.join("Cargo.toml");
285    let collection_toml_path = collection_path.join("Cargo.toml");
286
287    let mut collection_toml = read_to_string(collection_toml_path).expect("Cargo.toml not found");
288    if let Ok(new_path) = std::env::var("ZNAP_LIB") {
289        if let Some(line) = collection_toml
290            .lines()
291            .find(|l| l.trim().starts_with("znap = { path ="))
292        {
293            collection_toml =
294                collection_toml.replace(line, &format!("znap = {{ path = \"{new_path}\" }}"));
295        }
296    }
297    let znap_toml_extras = template::collection_toml::template(&collection.name);
298
299    write_file(
300        &znap_collection_toml_path,
301        &format!("{collection_toml}\n{znap_toml_extras}"),
302    );
303}
304
305pub fn build_for_release(name: &str) {
306    std::process::Command::new("cargo")
307        .arg("build")
308        .arg("--manifest-path")
309        .arg(get_cwd().join(format!(".znap/collections/{name}/Cargo.toml")))
310        .arg("--release")
311        .arg("--bin")
312        .arg("serve")
313        .stdout(Stdio::inherit())
314        .stderr(Stdio::inherit())
315        .spawn()
316        .map_err(anyhow::Error::from)
317        .expect("Should be able to build collection.")
318        .wait_with_output()
319        .expect("Should wait until the build is over");
320}