Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 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 crates/stellar-scaffold-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ doctest = false
openssl = { version = "0.10", features = ["vendored"] }
stellar-build = { path = "../stellar-build", version = "0.0.2" }
stellar-cli = { workspace = true}
soroban-rpc = { workspace = true}
clap = { version = "4.1.8", features = [
"derive",
"env",
Expand Down
132 changes: 106 additions & 26 deletions crates/stellar-scaffold-cli/src/commands/build/clients.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ pub enum Error {
GeneratingKey(#[from] cli::keys::generate::Error),
#[error("⛔ ️can only have one default account; marked as default: {0:?}")]
OnlyOneDefaultAccount(Vec<String>),
#[error(transparent)]
InvalidPublicKey(#[from] cli::keys::public_key::Error),
#[error(transparent)]
AddressParsing(#[from] stellar_cli::config::address::Error),
#[error("⛔ ️you need to provide at least one account, to use as the source account for contract deployment and other operations")]
NeedAtLeastOneAccount,
#[error("⛔ ️No contract named {0:?}")]
Expand All @@ -62,6 +66,8 @@ pub enum Error {
MissingContractID(String),
#[error("⛔ ️Unable to parse script: {0:?}")]
ScriptParseFailure(String),
#[error(transparent)]
RpcClient(#[from] soroban_rpc::Error),
#[error("⛔ ️Failed to execute subcommand: {0:?}\n{1:?}")]
SubCommandExecutionFailure(String, String),
#[error(transparent)]
Expand All @@ -88,6 +94,8 @@ pub enum Error {
Json(#[from] serde_json::Error),
#[error("⛔ ️Failed to run npm command in {0:?}: {1:?}")]
NpmCommandFailure(std::path::PathBuf, String),
#[error(transparent)]
AccountFund(#[from] cli::keys::fund::Error),
}

impl Args {
Expand All @@ -109,7 +117,9 @@ impl Args {
// Create the '.stellar' directory if it doesn't exist
std::fs::create_dir_all(workspace_root.join(".stellar"))
.map_err(stellar_cli::config::locator::Error::Io)?;
Self::handle_accounts(current_env.accounts.as_deref()).await?;
self.clone()
.handle_accounts(current_env.accounts.as_deref(), &current_env.network)
.await?;
self.clone()
.handle_contracts(
current_env.contracts.as_ref(),
Expand Down Expand Up @@ -288,6 +298,10 @@ export default new Client.Client({{
.to_str()
.expect("we do not support non-utf8 paths"),
"--overwrite",
"--config-dir",
workspace_root
.to_str()
.expect("we do not support non-utf8 paths"),
])?
.run()
.await?;
Expand Down Expand Up @@ -338,7 +352,11 @@ export default new Client.Client({{
Ok(())
}

async fn handle_accounts(accounts: Option<&[env_toml::Account]>) -> Result<(), Error> {
async fn handle_accounts(
self,
Comment thread
BlaineHeffron marked this conversation as resolved.
Outdated
accounts: Option<&[env_toml::Account]>,
network: &Network,
) -> Result<(), Error> {
let Some(accounts) = accounts else {
return Err(Error::NeedAtLeastOneAccount);
};
Expand All @@ -358,22 +376,57 @@ export default new Client.Client({{

for account in accounts {
eprintln!("🔐 creating keys for {:?}", account.name);
cli::keys::generate::Cmd::parse_arg_vec(&[&account.name, "--fund"])?
.run(&stellar_cli::commands::global::Args::default())
.await
.or_else(|e| {
if e.to_string().contains("already exists") {
// ignore "already exists" errors
eprintln!("{e}");
Ok(())
} else {
Err(e)
let args = stellar_cli::commands::global::Args {
locator: self.clone().get_config_locator(),
..Default::default()
};

let generate_cmd = cli::keys::generate::Cmd {
name: account.name.clone().parse()?,
fund: true,
config_locator: self.get_config_locator(),
network: Self::get_network_args(network),
seed: None,
hd_path: None,
no_fund: false,
as_secret: false,
secure_store: false,
overwrite: false,
};

match generate_cmd.run(&args).await {
Ok(()) => {}
Comment thread
BlaineHeffron marked this conversation as resolved.
Outdated
Err(e) if e.to_string().contains("already exists") => {
Comment thread
BlaineHeffron marked this conversation as resolved.
eprintln!("{e}");
// Check if account exists on chain
let rpc_client = soroban_rpc::Client::new(
network
.rpc_url
.as_ref()
.expect("network contains the RPC url"),
)?;

let public_key_cmd = cli::keys::public_key::Cmd {
name: account.name.clone().parse()?,
locator: self.get_config_locator(),
hd_path: None,
};
let address = public_key_cmd.public_key().await?;

if (rpc_client.get_account(&address.to_string()).await).is_err() {
eprintln!("Account not found on chain, funding...");
let fund_cmd = cli::keys::fund::Cmd {
network: Self::get_network_args(network),
address: public_key_cmd,
};
fund_cmd.run(&args).await?;
}
})?;
}
Err(e) => return Err(e.into()),
Comment thread
BlaineHeffron marked this conversation as resolved.
Outdated
}
Comment thread
BlaineHeffron marked this conversation as resolved.
}

std::env::set_var("STELLAR_ACCOUNT", &default_account);

Ok(())
}

Expand Down Expand Up @@ -525,21 +578,20 @@ export default new Client.Client({{

// Deploy new contract if we got here
let contract_id = self.deploy_contract(name, &hash, &settings).await?;
// Run after_deploy script if in development or test environment
if (env == "development" || env == "testing") && settings.after_deploy.is_some() {
eprintln!("🚀 Running after_deploy script for {name:?}");
self.run_after_deploy_script(
name,
&contract_id,
settings.after_deploy.as_ref().unwrap(),
)
.await?;
}
self.save_contract_alias(name, &contract_id, network)?;
contract_id
};

// Run after_deploy script if in development or test environment
if (env == "development" || env == "testing") && settings.after_deploy.is_some() {
eprintln!("🚀 Running after_deploy script for {name:?}");
self.run_after_deploy_script(
name,
&contract_id,
settings.after_deploy.as_ref().unwrap(),
)
.await?;
}

self.clone()
.generate_contract_bindings(name, &contract_id.to_string())
.await?;
Expand All @@ -553,11 +605,19 @@ export default new Client.Client({{
wasm_path: &std::path::Path,
) -> Result<String, Error> {
eprintln!("📲 installing {name:?} wasm bytecode on-chain...");
let workspace_root = self
.workspace_root
.as_ref()
.expect("workspace_root must be set before running");
let hash = cli::contract::upload::Cmd::parse_arg_vec(&[
"--wasm",
wasm_path
.to_str()
.expect("we do not support non-utf8 paths"),
"--config-dir",
workspace_root
.to_str()
.expect("we do not support non-utf8 paths"),
])?
.run_against_rpc_server(None, None)
.await?
Expand Down Expand Up @@ -603,11 +663,20 @@ export default new Client.Client({{
hash: &str,
settings: &env_toml::Contract,
) -> Result<Contract, Error> {
let workspace_root = self
.workspace_root
.as_ref()
.expect("workspace_root must be set before running");
let mut deploy_args = vec![
"--alias".to_string(),
name.to_string(),
"--wasm-hash".to_string(),
hash.to_string(),
"--config-dir".to_string(),
workspace_root
.to_str()
.expect("we do not support non-utf8 paths")
.to_string(),
];

if let Some(constructor_script) = &settings.constructor_args {
Expand Down Expand Up @@ -684,8 +753,19 @@ export default new Client.Client({{

let (source_account, command_parts) = Self::parse_script_line(line)?;

let workspace_root = self
.workspace_root
.as_ref()
.expect("workspace_root must be set before running");
let contract_id_arg = contract_id.to_string();
let mut args = vec!["--id", &contract_id_arg];
let mut args = vec![
"--id",
&contract_id_arg,
"--config-dir",
workspace_root
.to_str()
.expect("we do not support non-utf8 paths"),
];
if let Some(account) = source_account.as_ref() {
args.extend_from_slice(&["--source-account", account]);
}
Expand Down
37 changes: 37 additions & 0 deletions crates/stellar-scaffold-cli/src/commands/build/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ pub async fn start_local_stellar() -> Result<(), Box<dyn Error>> {
}
wait_for_stellar_health().await
}

async fn wait_for_stellar_health() -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
let start_time = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(60);

// First check Stellar RPC health
loop {
let elapsed_time = start_time.elapsed();
if elapsed_time > timeout {
Expand All @@ -37,6 +40,7 @@ async fn wait_for_stellar_health() -> Result<(), Box<dyn Error>> {
if res.status().is_success() {
let health_status: serde_json::Value = res.json().await?;
if health_status["result"]["status"] == "healthy" {
eprintln!("Stellar RPC is healthy, now checking friendbot...");
break;
}
eprintln!("Stellar status is not healthy: {health_status:?}");
Expand All @@ -46,5 +50,38 @@ async fn wait_for_stellar_health() -> Result<(), Box<dyn Error>> {
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
eprintln!("Retrying health check.");
}

// Now check friendbot readiness
loop {
let elapsed_time = start_time.elapsed();
if elapsed_time > timeout {
eprintln!("Timeout reached: friendbot check failed.");
return Err("Friendbot readiness check timed out".into());
}

// Use a dummy address to test friendbot availability
let res = client
.get("http://localhost:8000/friendbot?addr=GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
.send()
.await;

match res {
Ok(response) => {
if response.status().is_success() || response.status() == 400 {
// 400 is expected for invalid address, but means friendbot is responding
eprintln!("Friendbot is ready!");
break;
}
eprintln!("Friendbot not ready, status: {}", response.status());
}
Err(e) => {
eprintln!("Friendbot connection failed: {e}");
}
}

tokio::time::sleep(std::time::Duration::from_secs(2)).await;
eprintln!("Retrying friendbot check.");
}

Ok(())
}
36 changes: 36 additions & 0 deletions crates/stellar-scaffold-cli/tests/it/build_clients/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,39 @@ soroban_token_contract.client = false
assert!(stderr.contains("Account AliasOrSecret(\"alice\") funded"));
});
}

#[test]
fn funding_existing_account_toml() {
use std::fs;

TestEnv::from("soroban-init-boilerplate", |env| {
env.set_environments_toml(r#"
[development]
network = { rpc-url = "http://localhost:8000/rpc", network-passphrase = "Standalone Network ; February 2017"}

accounts = [
"alice",
]
[development.contracts]
soroban_hello_world_contract.client = true
soroban_increment_contract.client = false
soroban_custom_types_contract.client = false
soroban_auth_contract.client = false
soroban_token_contract.client = false
"#);

// Create alice.toml manually, simulating a pre-existing identity
let alice_toml_path = env.cwd.join(".stellar/identity/alice.toml");
let parent = alice_toml_path.parent().unwrap();
fs::create_dir_all(parent).unwrap();
fs::write(&alice_toml_path, r#"
seed_phrase = "own social that glimpse hurry lion arrange spot vault clip leisure innocent borrow peanut invest scrub network enter enemy digital uncover ivory expire peace"
"#).unwrap();

// Run scaffold_build and assert success
env.scaffold_build("development", true)
.assert()
.success()
.stderr_as_str();
});
}