1use anyhow::{anyhow, ensure};
18use clap::{Parser, Subcommand};
19use colored::Colorize;
20use entropy_client::{
21 chain_api::{
22 entropy::runtime_types::{
23 bounded_collections::bounded_vec::BoundedVec, pallet_registry::pallet::ProgramInstance,
24 },
25 EntropyConfig,
26 },
27 client::{
28 change_endpoint, change_threshold_accounts, get_accounts, get_api, get_programs, get_rpc,
29 jumpstart_network, register, remove_program, sign, store_program, update_programs,
30 VERIFYING_KEY_LENGTH,
31 },
32};
33pub use entropy_shared::PROGRAM_VERSION_NUMBER;
34use sp_core::{sr25519, Hasher, Pair};
35use sp_runtime::traits::BlakeTwo256;
36use std::{fs, path::PathBuf};
37use subxt::{
38 backend::legacy::LegacyRpcMethods,
39 utils::{AccountId32 as SubxtAccountId32, H256},
40 OnlineClient,
41};
42
43#[derive(Parser, Debug, Clone)]
44#[clap(
45 version,
46 about = "CLI tool for testing Entropy",
47 long_about = "This is a CLI test client.\nIt requires a running deployment of Entropy with at least two chain nodes and two TSS servers."
48)]
49struct Cli {
50 #[clap(subcommand)]
51 command: CliCommand,
52 #[arg(short, long)]
59 chain_endpoint: Option<String>,
60}
61
62#[derive(Subcommand, Debug, Clone)]
63enum CliCommand {
64 Register {
66 programs: Vec<String>,
78 #[arg(short, long)]
80 program_version_numbers: Option<Vec<u8>>,
81 #[arg(short, long)]
86 mnemonic_option: Option<String>,
87 },
88 Sign {
90 signature_verifying_key: String,
92 message: String,
94 auxilary_data: Option<String>,
96 #[arg(short, long)]
98 mnemonic_option: Option<String>,
99 },
100 UpdatePrograms {
102 signature_verifying_key: String,
104 programs: Vec<String>,
116 program_version_numbers: Option<Vec<u8>>,
118 #[arg(short, long)]
120 mnemonic_option: Option<String>,
121 },
122 StoreProgram {
124 program_file: Option<PathBuf>,
126 config_interface_file: Option<PathBuf>,
128 aux_data_interface_file: Option<PathBuf>,
130 program_version_number: Option<u8>,
132 #[arg(short, long)]
134 mnemonic_option: Option<String>,
135 },
136 RemoveProgram {
138 hash: String,
140 #[arg(short, long)]
142 mnemonic_option: Option<String>,
143 },
144 ChangeEndpoint {
146 new_endpoint: String,
148 #[arg(short, long)]
150 mnemonic_option: Option<String>,
151 },
152 ChangeThresholdAccounts {
154 new_tss_account: String,
156 new_x25519_public_key: String,
158 #[arg(short, long)]
160 mnemonic_option: Option<String>,
161 },
162 Status,
164 JumpstartNetwork {
171 #[arg(short, long)]
173 mnemonic_option: Option<String>,
174 },
175}
176
177pub async fn run_command(
178 program_file_option: Option<PathBuf>,
179 config_interface_file_option: Option<PathBuf>,
180 aux_data_interface_file_option: Option<PathBuf>,
181 program_version_number_option: Option<u8>,
182) -> anyhow::Result<String> {
183 let cli = Cli::parse();
184
185 let endpoint_addr = cli.chain_endpoint.unwrap_or_else(|| {
186 std::env::var("ENTROPY_DEVNET").unwrap_or("ws://localhost:9944".to_string())
187 });
188
189 let passed_mnemonic = std::env::var("DEPLOYER_MNEMONIC");
190
191 let api = get_api(&endpoint_addr).await?;
192 let rpc = get_rpc(&endpoint_addr).await?;
193
194 match cli.command {
195 CliCommand::Register { mnemonic_option, programs, program_version_numbers } => {
196 let mnemonic = if let Some(mnemonic_option) = mnemonic_option {
197 mnemonic_option
198 } else {
199 passed_mnemonic.expect("No mnemonic set")
200 };
201
202 let program_keypair = <sr25519::Pair as Pair>::from_string(&mnemonic, None)?;
203 let program_account = SubxtAccountId32(program_keypair.public().0);
204 println!("Program account: {}", program_keypair.public());
205
206 let mut programs_info = vec![];
207
208 for (i, program) in programs.into_iter().enumerate() {
209 let program_version_number =
210 program_version_numbers.as_ref().map_or(0u8, |versions| versions[i]);
211 programs_info.push(
212 Program::from_hash_or_filename(
213 &api,
214 &rpc,
215 &program_keypair,
216 program,
217 program_version_number,
218 )
219 .await?
220 .0,
221 );
222 }
223
224 let (verifying_key, registered_info) = register(
225 &api,
226 &rpc,
227 program_keypair.clone(),
228 program_account,
229 BoundedVec(programs_info),
230 )
231 .await?;
232
233 Ok(format!("Verifying key: {},\n{:?}", hex::encode(verifying_key), registered_info))
234 },
235 CliCommand::Sign { signature_verifying_key, message, auxilary_data, mnemonic_option } => {
236 let mnemonic = if let Some(mnemonic_option) = mnemonic_option {
237 mnemonic_option
238 } else {
239 passed_mnemonic.unwrap_or("//Alice".to_string())
240 };
241 let user_keypair = <sr25519::Pair as Pair>::from_string(&mnemonic, None)?;
243
244 println!("User account for current call: {}", user_keypair.public());
245
246 let auxilary_data =
247 if let Some(data) = auxilary_data { Some(hex::decode(data)?) } else { None };
248
249 let signature_verifying_key: [u8; VERIFYING_KEY_LENGTH] =
250 hex::decode(signature_verifying_key)?
251 .try_into()
252 .map_err(|_| anyhow!("Verifying key must be 33 bytes"))?;
253
254 let recoverable_signature = sign(
255 &api,
256 &rpc,
257 user_keypair,
258 signature_verifying_key,
259 message.as_bytes().to_vec(),
260 auxilary_data,
261 )
262 .await?;
263 Ok(format!("Message signed: {:?}", recoverable_signature))
264 },
265 CliCommand::StoreProgram {
266 mnemonic_option,
267 program_file,
268 config_interface_file,
269 aux_data_interface_file,
270 program_version_number,
271 } => {
272 let mnemonic = if let Some(mnemonic_option) = mnemonic_option {
273 mnemonic_option
274 } else {
275 passed_mnemonic.expect("No Mnemonic set")
276 };
277 let keypair = <sr25519::Pair as Pair>::from_string(&mnemonic, None)?;
278 println!("Storing program using account: {}", keypair.public());
279
280 let program = match program_file {
281 Some(file_name) => fs::read(file_name)?,
282 None => fs::read(program_file_option.expect("No program file passed in"))?,
283 };
284
285 let config_interface = match config_interface_file {
286 Some(file_name) => fs::read(file_name)?,
287 None => fs::read(
288 config_interface_file_option.expect("No config interface file passed"),
289 )?,
290 };
291
292 let aux_data_interface = match aux_data_interface_file {
293 Some(file_name) => fs::read(file_name)?,
294 None => fs::read(
295 aux_data_interface_file_option.expect("No aux data interface file passed"),
296 )?,
297 };
298
299 let program_version_number = match program_version_number_option {
300 Some(program_version_number) => program_version_number,
301 None => program_version_number.unwrap_or(0u8),
302 };
303
304 let hash = store_program(
305 &api,
306 &rpc,
307 &keypair,
308 program,
309 config_interface,
310 aux_data_interface,
311 vec![],
312 program_version_number,
313 )
314 .await?;
315 Ok(format!("Program stored: {}", hex::encode(hash)))
316 },
317 CliCommand::RemoveProgram { mnemonic_option, hash } => {
318 let mnemonic = if let Some(mnemonic_option) = mnemonic_option {
319 mnemonic_option
320 } else {
321 passed_mnemonic.expect("No Mnemonic set")
322 };
323 let keypair = <sr25519::Pair as Pair>::from_string(&mnemonic, None)?;
324 println!("Removing program using account: {}", keypair.public());
325
326 let hash: [u8; 32] = hex::decode(hash)?
327 .try_into()
328 .map_err(|_| anyhow!("Program hash must be 32 bytes"))?;
329
330 remove_program(&api, &rpc, &keypair, H256(hash)).await?;
331
332 Ok("Program removed".to_string())
333 },
334 CliCommand::UpdatePrograms {
335 signature_verifying_key,
336 mnemonic_option,
337 programs,
338 program_version_numbers,
339 } => {
340 let mnemonic = if let Some(mnemonic_option) = mnemonic_option {
341 mnemonic_option
342 } else {
343 passed_mnemonic.expect("No Mnemonic set")
344 };
345 let program_keypair = <sr25519::Pair as Pair>::from_string(&mnemonic, None)?;
346 println!("Program account: {}", program_keypair.public());
347
348 let mut programs_info = Vec::new();
349
350 for (i, program) in programs.into_iter().enumerate() {
351 let program_version_number =
352 program_version_numbers.as_ref().map_or(0u8, |versions| versions[i]);
353 programs_info.push(
354 Program::from_hash_or_filename(
355 &api,
356 &rpc,
357 &program_keypair,
358 program,
359 program_version_number,
360 )
361 .await?
362 .0,
363 );
364 }
365
366 let verifying_key: [u8; VERIFYING_KEY_LENGTH] = hex::decode(signature_verifying_key)?
367 .try_into()
368 .map_err(|_| anyhow!("Verifying key must be 33 bytes"))?;
369
370 update_programs(&api, &rpc, verifying_key, &program_keypair, BoundedVec(programs_info))
371 .await?;
372
373 Ok("Programs updated".to_string())
374 },
375 CliCommand::Status => {
376 let accounts = get_accounts(&api, &rpc).await?;
377 println!(
378 "There are {} registered Entropy accounts.\n",
379 accounts.len().to_string().green()
380 );
381 if !accounts.is_empty() {
382 println!(
383 "{:<64} {:<12} Programs:",
384 "Verifying key:".green(),
385 "Visibility:".purple(),
386 );
387 for (account_id, info) in accounts {
388 println!(
389 "{} {}",
390 hex::encode(account_id).green(),
391 format!(
392 "{:?}",
393 info.programs_data
394 .0
395 .iter()
396 .map(|program_instance| format!(
397 "{}",
398 program_instance.program_pointer
399 ))
400 .collect::<Vec<_>>()
401 )
402 .white(),
403 );
404 }
405 }
406
407 let programs = get_programs(&api, &rpc).await?;
408
409 println!("\nThere are {} stored programs\n", programs.len().to_string().green());
410
411 if !programs.is_empty() {
412 println!(
413 "{:<64} {:<48} {:<11} {:<14} {} {}",
414 "Hash".blue(),
415 "Stored by:".green(),
416 "Times used:".purple(),
417 "Size in bytes:".cyan(),
418 "Configurable?".yellow(),
419 "Has auxiliary?".yellow(),
420 );
421 for (hash, program_info) in programs {
422 println!(
423 "{} {} {:>11} {:>14} {:<13} {}",
424 hex::encode(hash),
425 program_info.deployer,
426 program_info.ref_counter,
427 program_info.bytecode.len(),
428 !program_info.configuration_schema.is_empty(),
429 !program_info.auxiliary_data_schema.is_empty(),
430 );
431 }
432 }
433
434 Ok("Got status".to_string())
435 },
436 CliCommand::ChangeEndpoint { new_endpoint, mnemonic_option } => {
437 let mnemonic = if let Some(mnemonic_option) = mnemonic_option {
438 mnemonic_option
439 } else {
440 passed_mnemonic.expect("No Mnemonic set")
441 };
442
443 let user_keypair = <sr25519::Pair as Pair>::from_string(&mnemonic, None)?;
444 println!("User account for current call: {}", user_keypair.public());
445
446 let result_event = change_endpoint(&api, &rpc, user_keypair, new_endpoint).await?;
447 println!("Event result: {:?}", result_event);
448 Ok("Endpoint changed".to_string())
449 },
450 CliCommand::ChangeThresholdAccounts {
451 new_tss_account,
452 new_x25519_public_key,
453 mnemonic_option,
454 } => {
455 let mnemonic = if let Some(mnemonic_option) = mnemonic_option {
456 mnemonic_option
457 } else {
458 passed_mnemonic.expect("No Mnemonic set")
459 };
460 let user_keypair = <sr25519::Pair as Pair>::from_string(&mnemonic, None)?;
461 println!("User account for current call: {}", user_keypair.public());
462
463 let result_event = change_threshold_accounts(
464 &api,
465 &rpc,
466 user_keypair,
467 new_tss_account,
468 new_x25519_public_key,
469 )
470 .await?;
471 println!("Event result: {:?}", result_event);
472
473 Ok("Threshold accounts changed".to_string())
474 },
475 CliCommand::JumpstartNetwork { mnemonic_option } => {
476 let mnemonic = if let Some(mnemonic_option) = mnemonic_option {
477 mnemonic_option
478 } else {
479 passed_mnemonic.unwrap_or("//Alice".to_string())
480 };
481
482 let signer = <sr25519::Pair as Pair>::from_string(&mnemonic, None)?;
483 println!("Account being used for jumpstart: {}", signer.public());
484
485 jumpstart_network(&api, &rpc, signer).await?;
486
487 Ok("Succesfully jumpstarted network.".to_string())
488 },
489 }
490}
491
492struct Program(ProgramInstance);
493
494impl Program {
495 fn new(program_pointer: H256, program_config: Vec<u8>) -> Self {
496 Self(ProgramInstance { program_pointer, program_config })
497 }
498
499 async fn from_hash_or_filename(
500 api: &OnlineClient<EntropyConfig>,
501 rpc: &LegacyRpcMethods<EntropyConfig>,
502 keypair: &sr25519::Pair,
503 hash_or_filename: String,
504 program_version_number: u8,
505 ) -> anyhow::Result<Self> {
506 match hex::decode(hash_or_filename.clone()) {
507 Ok(hash) => {
508 let hash_32_res: Result<[u8; 32], _> = hash.try_into();
509 match hash_32_res {
510 Ok(hash_32) => {
511 let configuration = {
514 let mut configuration_file = PathBuf::from(&hash_or_filename);
515 configuration_file.set_extension("json");
516 fs::read(&configuration_file).unwrap_or_default()
517 };
518 Ok(Self::new(H256(hash_32), configuration))
519 },
520 Err(_) => {
521 Self::from_file(api, rpc, keypair, hash_or_filename, program_version_number)
522 .await
523 },
524 }
525 },
526 Err(_) => {
527 Self::from_file(api, rpc, keypair, hash_or_filename, program_version_number).await
528 },
529 }
530 }
531
532 async fn from_file(
535 api: &OnlineClient<EntropyConfig>,
536 rpc: &LegacyRpcMethods<EntropyConfig>,
537 keypair: &sr25519::Pair,
538 filename: String,
539 program_version_number: u8,
540 ) -> anyhow::Result<Self> {
541 let program_bytecode = fs::read(&filename)?;
542
543 let config_description = {
545 let mut config_description_file = PathBuf::from(&filename);
546 config_description_file.set_extension("config-description");
547 fs::read(&config_description_file).unwrap_or_default()
548 };
549
550 let auxiliary_data_schema = {
552 let mut auxiliary_data_schema_file = PathBuf::from(&filename);
553 auxiliary_data_schema_file.set_extension("aux-description");
554 fs::read(&auxiliary_data_schema_file).unwrap_or_default()
555 };
556
557 let configuration = {
559 let mut configuration_file = PathBuf::from(&filename);
560 configuration_file.set_extension("json");
561 fs::read(&configuration_file).unwrap_or_default()
562 };
563
564 ensure!(
565 (config_description.is_empty() && configuration.is_empty())
566 || (!config_description.is_empty() && !configuration.is_empty()),
567 "If giving an interface description you must also give a configuration"
568 );
569
570 match store_program(
571 api,
572 rpc,
573 keypair,
574 program_bytecode.clone(),
575 config_description,
576 auxiliary_data_schema,
577 vec![],
578 program_version_number,
579 )
580 .await
581 {
582 Ok(hash) => Ok(Self::new(hash, configuration)),
583 Err(error) => {
584 if error.to_string().ends_with("ProgramAlreadySet") {
585 println!("Program is already stored - using existing one");
586 let hash = BlakeTwo256::hash(&program_bytecode);
587 Ok(Self::new(H256(hash.into()), configuration))
588 } else {
589 Err(error.into())
590 }
591 },
592 }
593 }
594}