Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(CLI): use aliases to avoid needlessly redeploying contracts #88

Merged
merged 23 commits into from
Jul 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
38 changes: 36 additions & 2 deletions Cargo.lock

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

5 changes: 5 additions & 0 deletions crates/loam-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,16 @@ itertools = "0.12.1"
strsim = "0.11.1"
heck = "0.5.0"
pathdiff = "0.2.1"
sha2 = "0.10.7"
hex = "0.4.3"
shlex = "1.1.0"
symlink = "0.1.0"
toml = { version = "0.8.12", features = ["parse"] }
rand = "0.8.5"
wasm-gen = { version = "0.1.4" }
soroban-cli = "21.0.0"
notify = "5.0"
stellar-xdr = "21.0.0"
rust-embed = { version = "8.2.0", features = ["debug-embed"] }

[dev-dependencies]
Expand Down
170 changes: 139 additions & 31 deletions crates/loam-cli/src/commands/build/clients.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
#![allow(clippy::struct_excessive_bools)]
use crate::commands::build::env_toml;
use serde_json;
use soroban_cli::commands::NetworkRunnable;
use soroban_cli::utils::contract_hash;
use soroban_cli::{commands as cli, CommandParser};
use std::collections::BTreeMap as Map;
use std::fmt::Debug;
use std::hash::Hash;
use stellar_xdr::curr::Error as xdrError;

use super::env_toml::Network;

Expand Down Expand Up @@ -43,14 +47,26 @@ pub enum Error {
NeedAtLeastOneAccount,
#[error("⛔ ️No contract named {0:?}")]
BadContractName(String),
#[error("⛔ ️Contract update not allowed in production for {0:?}")]
ContractUpdateNotAllowed(String),
#[error(transparent)]
ContractInstall(#[from] cli::contract::install::Error),
#[error(transparent)]
ContractDeploy(#[from] cli::contract::deploy::wasm::Error),
#[error(transparent)]
ContractBindings(#[from] cli::contract::bindings::typescript::Error),
#[error(transparent)]
ContractFetch(#[from] cli::contract::fetch::Error),
#[error(transparent)]
ConfigLocator(#[from] cli::config::locator::Error),
#[error(transparent)]
Clap(#[from] clap::Error),
#[error(transparent)]
WasmHash(#[from] xdrError),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Json(#[from] serde_json::Error),
}

impl Args {
Expand All @@ -63,8 +79,12 @@ impl Args {

Self::add_network_to_env(&current_env.network)?;
Self::handle_accounts(current_env.accounts.as_deref()).await?;
self.handle_contracts(workspace_root, current_env.contracts.as_ref())
.await?;
self.handle_contracts(
workspace_root,
current_env.contracts.as_ref(),
&current_env.network,
)
.await?;

Ok(())
}
Expand Down Expand Up @@ -115,6 +135,89 @@ impl Args {
Ok(())
}

fn get_network_args(network: &Network) -> cli::network::Args {
cli::network::Args {
rpc_url: network.rpc_url.clone(),
network_passphrase: network.network_passphrase.clone(),
network: network.name.clone(),
}
}

fn get_config_locator() -> cli::config::locator::Args {
cli::config::locator::Args {
global: false,
config_dir: None,
}
}

fn get_contract_alias(name: &str) -> Result<Option<String>, cli::config::locator::Error> {
let config_dir = Self::get_config_locator();
let network_passphrase = std::env::var("STELLAR_NETWORK_PASSPHRASE")
.expect("No STELLAR_NETWORK_PASSPHRASE environment variable set");
config_dir.get_contract_id(name, &network_passphrase)
}

async fn contract_hash_matches(
&self,
contract_id: &str,
hash: &str,
network: &Network,
) -> Result<bool, Error> {
let hash_vec = cli::contract::fetch::Cmd {
contract_id: contract_id.to_string(),
out_file: None,
locator: Self::get_config_locator(),
network: Self::get_network_args(network),
}
.run_against_rpc_server(None, None)
.await?;
let ctrct_hash = contract_hash(&hash_vec)?;
Ok(hex::encode(ctrct_hash) == hash)
}

fn save_contract_alias(
name: &str,
contract_id: &str,
network: &Network,
) -> Result<(), cli::config::locator::Error> {
let config_dir = Self::get_config_locator();
let passphrase = network
.network_passphrase
.clone()
.expect("You must set the network passphrase");
config_dir.save_contract_id(&passphrase, contract_id, name)
}

fn write_contract_template(
self,
workspace_root: &std::path::Path,
name: &str,
contract_id: &str,
) -> Result<(), Error> {
let allow_http = if self.loam_env(LoamEnv::Production) == "development" {
"\n allowHttp: true,"
} else {
""
};
let network = std::env::var("STELLAR_NETWORK_PASSPHRASE")
.expect("No STELLAR_NETWORK_PASSPHRASE environment variable set");
let template = format!(
r#"import * as Client from '{name}';
import {{ rpcUrl }} from './util';

export default new Client.Client({{
networkPassphrase: '{network}',
contractId: '{contract_id}',
rpcUrl,{allow_http}
publicKey: undefined,
}});
"#
);
let path = workspace_root.join(format!("src/contracts/{name}.ts"));
std::fs::write(path, template)?;
Ok(())
}

async fn handle_accounts(accounts: Option<&[env_toml::Account]>) -> Result<(), Error> {
let Some(accounts) = accounts else {
return Err(Error::NeedAtLeastOneAccount);
Expand Down Expand Up @@ -149,6 +252,7 @@ impl Args {
&self,
workspace_root: &std::path::Path,
contracts: Option<&Map<Box<str>, env_toml::Contract>>,
network: &Network,
) -> Result<(), Error> {
let Some(contracts) = contracts else {
return Ok(());
Expand All @@ -173,17 +277,41 @@ impl Args {
.to_string();
eprintln!(" ↳ hash: {hash}");

// Check if we have an alias saved for this contract
let alias = Self::get_contract_alias(name)?;
if let Some(contract_id) = alias {
match self
.contract_hash_matches(&contract_id, &hash, network)
.await
{
Ok(true) => {
eprintln!("✅ Contract {name:?} is up to date");
continue;
}
Ok(false) if self.loam_env(LoamEnv::Production) == "production" => {
return Err(Error::ContractUpdateNotAllowed(name.to_string()));
}
Ok(false) => eprintln!("🔄 Updating contract {name:?}"),
Err(e) => return Err(e),
}
}

eprintln!("🪞 instantiating {name:?} smart contract");
// TODO: check if hash is already the installed version, skip the rest if so
let contract_id =
cli::contract::deploy::wasm::Cmd::parse_arg_vec(&["--wasm-hash", &hash])?
.run_against_rpc_server(None, None)
.await?
.into_result()
.expect("no contract id returned by 'contract deploy'");
// TODO: save the contract id for use in subsequent runs
let contract_id = cli::contract::deploy::wasm::Cmd::parse_arg_vec(&[
"--alias",
name,
"--wasm-hash",
&hash,
])?
.run_against_rpc_server(None, None)
.await?
.into_result()
.expect("no contract id returned by 'contract deploy'");
eprintln!(" ↳ contract_id: {contract_id}");

// Save the alias for future use
Self::save_contract_alias(name, &contract_id, network)?;

eprintln!("🎭 binding {name:?} contract");
cli::contract::bindings::typescript::Cmd::parse_arg_vec(&[
"--contract-id",
Expand All @@ -199,27 +327,7 @@ impl Args {
.await?;

eprintln!("🍽️ importing {:?} contract", name.clone());
let allow_http = if self.loam_env(LoamEnv::Production) == "development" {
"\n allowHttp: true,"
} else {
""
};
let network = std::env::var("STELLAR_NETWORK_PASSPHRASE")
.expect("No STELLAR_NETWORK_PASSPHRASE environment variable set");
let template = format!(
r#"import * as Client from '{name}';
import {{ rpcUrl }} from './util';

export default new Client.Client({{
networkPassphrase: '{network}',
contractId: '{contract_id}',
rpcUrl,{allow_http}
publicKey: undefined,
}});
"#
);
let path = workspace_root.join(format!("src/contracts/{name}.ts"));
std::fs::write(path, template).expect("could not write contract template");
self.write_contract_template(workspace_root, name, &contract_id)?;
};
}

Expand Down
13 changes: 9 additions & 4 deletions crates/loam-cli/src/commands/build/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,11 @@ pub struct Cmd {
/// Print commands to build without executing them
#[arg(long, conflicts_with = "out_dir", help_heading = "Other")]
pub print_commands_only: bool,
/// Build client code in addition to building the contract
#[arg(long)]
pub build_clients: bool,
#[command(flatten)]
pub build_clients: clients::Args,
pub build_clients_args: clients::Args,
}

#[derive(thiserror::Error, Debug)]
Expand Down Expand Up @@ -183,9 +186,11 @@ impl Cmd {
}
}

self.build_clients
.run(&metadata.workspace_root.into_std_path_buf())
.await?;
if self.build_clients {
self.build_clients_args
.run(&metadata.workspace_root.into_std_path_buf())
.await?;
}

Ok(())
}
Expand Down
2 changes: 1 addition & 1 deletion crates/loam-cli/src/commands/dev/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ impl Cmd {

fn cloned_build_command(&mut self) -> Arc<Mutex<build::Cmd>> {
self.build_cmd
.build_clients
.build_clients_args
.env
.get_or_insert(LoamEnv::Development);
self.build_cmd
Expand Down
Loading
Loading