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 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 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}