Skip to content

Commit

Permalink
Default account mechanism for sncast (#2773)
Browse files Browse the repository at this point in the history
<!-- Reference any GitHub issues resolved by this PR -->

Closes #2695

## Introduced changes

<!-- A brief description of the changes -->

- Added interactive prompt when creating or importing new account
- Added `silent` flag, to disable interactive selection dialog

## Checklist

<!-- Make sure all of these are complete -->

- [x] Linked relevant issue
- [x] Updated relevant documentation
- [x] Added relevant tests
- [x] Performed self-review of the code
- [x] Added changes to `CHANGELOG.md`
  • Loading branch information
kkawula authored Dec 20, 2024
1 parent 2049231 commit 327477d
Show file tree
Hide file tree
Showing 12 changed files with 332 additions and 6 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Cast

#### Added

- interactive interface that allows setting created or imported account as the default


## [0.35.1] - 2024-12-16

### Forge
Expand Down
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ cairo-lang-macro = "0.1.0"
cairo-vm = "1.0.0-rc3"
cairo-annotations = "0.1.0"
dirs = "5.0.1"
dialoguer = "0.11.0"
starknet-types-core = { version = "0.1.7", features = ["hash", "prime-bigint"] }
anyhow = "1.0.93"
assert_fs = "1.1.2"
Expand Down
2 changes: 2 additions & 0 deletions crates/sncast/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ walkdir.workspace = true
const-hex.workspace = true
regex.workspace = true
dirs.workspace = true
dialoguer.workspace = true
toml_edit.workspace = true

[dev-dependencies]
ctor.workspace = true
Expand Down
266 changes: 266 additions & 0 deletions crates/sncast/src/helpers/interactive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
use crate::helpers::config::get_global_config_path;
use anyhow::{Context, Result};
use camino::Utf8PathBuf;
use configuration::search_config_upwards_relative_to;
use dialoguer::theme::ColorfulTheme;
use dialoguer::Select;
use std::fmt::Display;
use std::{env, fs};
use toml_edit::{DocumentMut, Item, Table, Value};

enum PromptSelection {
GlobalDefault(Utf8PathBuf),
LocalDefault(Utf8PathBuf),
No,
}

impl Display for PromptSelection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PromptSelection::LocalDefault(path) => {
write!(f, "Yes, local default ({})", to_tilde_path(path))
}
PromptSelection::GlobalDefault(path) => {
write!(f, "Yes, global default ({})", to_tilde_path(path))
}
PromptSelection::No => write!(f, "No"),
}
}
}

pub fn prompt_to_add_account_as_default(account: &str) -> Result<()> {
let mut options = Vec::new();

if let Ok(global_path) = get_global_config_path() {
options.push(PromptSelection::GlobalDefault(global_path));
}

if let Some(local_path) = env::current_dir()
.ok()
.and_then(|current_path| Utf8PathBuf::from_path_buf(current_path.clone()).ok())
.and_then(|current_path_utf8| search_config_upwards_relative_to(&current_path_utf8).ok())
{
options.push(PromptSelection::LocalDefault(local_path));
}

options.push(PromptSelection::No);

let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Do you want to make this account default?")
.items(&options)
.default(0)
.interact()
.context("Failed to display selection dialog")?;

match &options[selection] {
PromptSelection::GlobalDefault(path) | PromptSelection::LocalDefault(path) => {
edit_config(path, "default", "account", account)?;
}
PromptSelection::No => {}
}

Ok(())
}

fn edit_config(config_path: &Utf8PathBuf, profile: &str, key: &str, value: &str) -> Result<()> {
let file_content = fs::read_to_string(config_path).context("Failed to read config file")?;

let mut toml_doc = file_content
.parse::<DocumentMut>()
.context("Failed to parse TOML")?;
update_config(&mut toml_doc, profile, key, value);

fs::write(config_path, toml_doc.to_string()).context("Failed to write to config file")?;

Ok(())
}

fn update_config(toml_doc: &mut DocumentMut, profile: &str, key: &str, value: &str) {
if !toml_doc.contains_key("sncast") {
toml_doc["sncast"] = Item::Table(Table::new());
}

let sncast_table = toml_doc
.get_mut("sncast")
.and_then(|item| item.as_table_mut())
.expect("Failed to create or access 'sncast' table");

if !sncast_table.contains_key(profile) {
sncast_table[profile] = Item::Table(Table::new());
}

let profile_table = sncast_table
.get_mut(profile)
.and_then(|item| item.as_table_mut())
.expect("Failed to create or access profile table");

profile_table[key] = Value::from(value).into();
}

fn to_tilde_path(path: &Utf8PathBuf) -> String {
if cfg!(not(target_os = "windows")) {
if let Some(home_dir) = dirs::home_dir() {
if let Ok(canonical_path) = path.canonicalize() {
if let Ok(stripped_path) = canonical_path.strip_prefix(&home_dir) {
return format!("~/{}", stripped_path.to_string_lossy());
}
}
}
}

path.to_string()
}

#[cfg(test)]
mod tests {

use super::update_config;
use indoc::formatdoc;
use toml_edit::DocumentMut;
#[test]
fn test_update_value() {
let original = formatdoc! {r#"
[snfoundry]
key = 2137
[sncast.default]
account = "mainnet"
url = "https://localhost:5050"
# comment
[sncast.testnet]
account = "testnet-account" # comment
url = "https://swmansion.com/"
"#};

let expected = formatdoc! {r#"
[snfoundry]
key = 2137
[sncast.default]
account = "testnet"
url = "https://localhost:5050"
# comment
[sncast.testnet]
account = "testnet-account" # comment
url = "https://swmansion.com/"
"#};

let mut toml_doc = original.parse::<DocumentMut>().unwrap();

update_config(&mut toml_doc, "default", "account", "testnet");

assert_eq!(toml_doc.to_string(), expected);
}

#[test]
fn test_create_key() {
let original = formatdoc! {r#"
[snfoundry]
key = 2137
[sncast.default]
url = "https://localhost:5050"
[sncast.testnet]
account = "testnet-account" # comment
url = "https://swmansion.com/"
"#};

let expected = formatdoc! {r#"
[snfoundry]
key = 2137
[sncast.default]
url = "https://localhost:5050"
account = "testnet"
[sncast.testnet]
account = "testnet-account" # comment
url = "https://swmansion.com/"
"#};

let mut toml_doc = original.parse::<DocumentMut>().unwrap();

update_config(&mut toml_doc, "default", "account", "testnet");

assert_eq!(toml_doc.to_string(), expected);
}

#[test]
fn test_create_table_key() {
let original = formatdoc! {r#"
[snfoundry]
key = 2137
[sncast.testnet]
account = "testnet-account" # comment
url = "https://swmansion.com/"
"#};

let expected = formatdoc! {r#"
[snfoundry]
key = 2137
[sncast.testnet]
account = "testnet-account" # comment
url = "https://swmansion.com/"
[sncast.default]
account = "testnet"
"#};

let mut toml_doc = original.parse::<DocumentMut>().unwrap();

update_config(&mut toml_doc, "default", "account", "testnet");

assert_eq!(toml_doc.to_string(), expected);
}

#[test]
fn test_create_table() {
let original = formatdoc! {r#"
[snfoundry]
key = 2137
"#};

let expected = formatdoc! {
r#"
[snfoundry]
key = 2137
[sncast]
[sncast.default]
account = "testnet"
"#};

let mut toml_doc = original.parse::<DocumentMut>().unwrap();

update_config(&mut toml_doc, "default", "account", "testnet");

assert_eq!(toml_doc.to_string(), expected);
}

#[test]
fn test_create_table_empty_file() {
let original = "";

let expected = formatdoc! {
r#"
[sncast]
[sncast.default]
account = "testnet"
"#};

let mut toml_doc = original.parse::<DocumentMut>().unwrap();

update_config(&mut toml_doc, "default", "account", "testnet");

assert_eq!(toml_doc.to_string(), expected);
}
}
1 change: 1 addition & 0 deletions crates/sncast/src/helpers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ pub mod configuration;
pub mod constants;
pub mod error;
pub mod fee;
pub mod interactive;
pub mod rpc;
pub mod scarb_utils;
19 changes: 19 additions & 0 deletions crates/sncast/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use anyhow::{Context, Result};
use data_transformer::Calldata;
use sncast::response::explorer_link::print_block_explorer_link_if_allowed;
use sncast::response::print::{print_command_result, OutputFormat};
use std::io;
use std::io::IsTerminal;

use crate::starknet_commands::deploy::DeployArguments;
use camino::Utf8PathBuf;
Expand All @@ -15,6 +17,7 @@ use sncast::helpers::config::{combine_cast_configs, get_global_config_path};
use sncast::helpers::configuration::CastConfig;
use sncast::helpers::constants::{DEFAULT_ACCOUNTS_FILE, DEFAULT_MULTICALL_CONTENTS};
use sncast::helpers::fee::PayableTransaction;
use sncast::helpers::interactive::prompt_to_add_account_as_default;
use sncast::helpers::scarb_utils::{
assert_manifest_path_exists, build, build_and_load_artifacts, get_package_metadata,
get_scarb_metadata_with_deps, BuildConfig,
Expand Down Expand Up @@ -489,6 +492,16 @@ async fn run_async_command(
)
.await;

if !import.silent && result.is_ok() && io::stdout().is_terminal() {
if let Some(account_name) =
result.as_ref().ok().and_then(|r| r.account_name.clone())
{
if let Err(err) = prompt_to_add_account_as_default(account_name.as_str()) {
eprintln!("Error: Failed to launch interactive prompt: {err}");
}
}
}

print_command_result("account import", &result, numbers_format, output_format)?;
Ok(())
}
Expand Down Expand Up @@ -516,6 +529,12 @@ async fn run_async_command(
)
.await;

if !create.silent && result.is_ok() && io::stdout().is_terminal() {
if let Err(err) = prompt_to_add_account_as_default(&account) {
eprintln!("Error: Failed to launch interactive prompt: {err}");
}
}

print_command_result("account create", &result, numbers_format, output_format)?;
print_block_explorer_link_if_allowed(
&result,
Expand Down
4 changes: 4 additions & 0 deletions crates/sncast/src/starknet_commands/account/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ pub struct Create {

#[clap(flatten)]
pub rpc: RpcArgs,

/// If passed, the command will not trigger an interactive prompt to add an account as a default
#[clap(long)]
pub silent: bool,
}

#[allow(clippy::too_many_arguments)]
Expand Down
Loading

0 comments on commit 327477d

Please sign in to comment.