Skip to content

Commit e3d1beb

Browse files
authored
feat(pop-cli): add --json support for up contract and network (#991)
* feat: add json support for up contract and network * fix: address json up review issues * fix: resolve no-default-features warning failures * chore: trigger ci for pr 991 * fix: validate --execute early in up json contract flow * fix: satisfy rustfmt import ordering in commands module * fix(pop-cli): cfg-gate up network JSON types * fix(pop-cli): restore up json path after rebase
1 parent 5224f7d commit e3d1beb

File tree

6 files changed

+656
-9
lines changed

6 files changed

+656
-9
lines changed

crates/pop-cli/src/commands/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ impl Command {
136136
#[cfg(feature = "chain")]
137137
Self::Bench(_) => true,
138138
#[cfg(any(feature = "chain", feature = "contract"))]
139+
Self::Up(_) => true,
140+
#[cfg(any(feature = "chain", feature = "contract"))]
139141
_ => false,
140142
}
141143
}
@@ -275,6 +277,11 @@ impl Command {
275277
#[cfg(any(feature = "chain", feature = "contract"))]
276278
Self::Up(args) => {
277279
env_logger::init();
280+
if output_mode == OutputMode::Json {
281+
let output = up::execute_json(args).await?;
282+
CliResponse::ok(output).print_json();
283+
return Ok(());
284+
}
278285
match &mut args.command {
279286
None => up::Command::execute(args).await,
280287
Some(cmd) => match cmd {
@@ -664,4 +671,16 @@ mod tests {
664671
fn test_command_supports_json() {
665672
assert!(Command::Test(test::TestArgs::default()).supports_json());
666673
}
674+
675+
#[test]
676+
fn up_command_supports_json() {
677+
let command = Command::Up(
678+
up::UpArgs {
679+
command: Some(up::Command::Network(Default::default())),
680+
..Default::default()
681+
}
682+
.into(),
683+
);
684+
assert!(command.supports_json());
685+
}
667686
}

crates/pop-cli/src/commands/up/contract.rs

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// SPDX-License-Identifier: GPL-3.0
22

3+
use super::UpContractOutput;
34
use crate::{
45
cli::{
5-
Cli,
6+
Cli, JsonCli,
67
traits::{Cli as _, Confirm},
78
},
89
commands::call::contract::CallContractCommand,
@@ -16,6 +17,7 @@ use crate::{
1617
urls,
1718
wallet::request_signature,
1819
},
20+
output::deploy_error,
1921
style::style,
2022
};
2123
use clap::Args;
@@ -497,6 +499,112 @@ impl UpContractCommand {
497499
Ok(())
498500
}
499501

502+
/// Executes the contract deployment in JSON mode and returns structured output.
503+
pub(crate) async fn execute_json(&mut self) -> anyhow::Result<UpContractOutput> {
504+
self.skip_confirm = true;
505+
let mut json_cli = JsonCli;
506+
if self.use_wallet {
507+
return Err(deploy_error(
508+
"`pop --json up` does not support `--use-wallet`; provide `--suri`",
509+
));
510+
}
511+
if self.upload_only {
512+
return Err(deploy_error(
513+
"`pop --json up` does not support `--upload-only`; deployment must instantiate a contract",
514+
));
515+
}
516+
if !self.execute {
517+
return Err(deploy_error(
518+
"`pop --json up` requires `--execute` to return contract deployment output",
519+
));
520+
}
521+
522+
let contract_already_built = has_contract_been_built(&self.path);
523+
if (!self.skip_build || !contract_already_built) &&
524+
let Err(e) =
525+
build_contract_artifacts(&mut json_cli, &self.path, true, Verbosity::Quiet, None)
526+
{
527+
return Err(deploy_error(e.to_string()));
528+
}
529+
530+
resolve_signer(self.skip_confirm, &mut self.use_wallet, &mut self.suri, &mut json_cli)
531+
.map_err(|e| deploy_error(e.to_string()))?;
532+
533+
let mut url = if let Some(url) = self.url.clone() {
534+
url
535+
} else {
536+
Url::parse(urls::LOCAL).expect("default url is valid")
537+
};
538+
let mut processes =
539+
if !is_chain_alive(url.clone()).await.map_err(|e| deploy_error(e.to_string()))? {
540+
let local_url = Url::parse(urls::LOCAL).expect("default url is valid");
541+
if url == local_url {
542+
let ports = resolve_ink_node_ports(DEFAULT_PORT, DEFAULT_ETH_RPC_PORT)
543+
.map_err(|e| deploy_error(e.to_string()))?;
544+
url = Url::parse(&format!("ws://localhost:{}", ports.ink_node_port))
545+
.map_err(|e| deploy_error(e.to_string()))?;
546+
Some(
547+
start_ink_node(&url, true, ports.ink_node_port, ports.eth_rpc_port)
548+
.await
549+
.map_err(|e| deploy_error(e.to_string()))?,
550+
)
551+
} else {
552+
return Err(deploy_error(
553+
"You need to specify an accessible endpoint to deploy the contract.",
554+
));
555+
}
556+
} else {
557+
None
558+
};
559+
self.url = Some(url.clone());
560+
561+
let deploy_result = async {
562+
let function =
563+
extract_function(self.path.clone(), &self.constructor, FunctionType::Constructor)
564+
.map_err(|e| deploy_error(e.to_string()))?;
565+
if !function.args.is_empty() {
566+
resolve_function_args(&function, &mut json_cli, &mut self.args, self.skip_confirm)
567+
.map_err(|e| deploy_error(e.to_string()))?;
568+
}
569+
normalize_call_args(&mut self.args, &function);
570+
571+
let instantiate_exec = set_up_deployment(self.clone().into())
572+
.await
573+
.map_err(|e| deploy_error(e.to_string()))?;
574+
575+
let calculated_weight = dry_run_gas_estimate_instantiate(&instantiate_exec)
576+
.await
577+
.map_err(|e| deploy_error(e.to_string()))?;
578+
let weight_limit =
579+
if let (Some(gas_limit), Some(proof_size)) = (self.gas_limit, self.proof_size) {
580+
Weight::from_parts(gas_limit, proof_size)
581+
} else {
582+
calculated_weight
583+
};
584+
585+
let contract_info = instantiate_smart_contract(instantiate_exec, weight_limit)
586+
.await
587+
.map_err(|e| deploy_error(e.to_string()))?;
588+
589+
Ok(UpContractOutput {
590+
contract_address: contract_info.address.to_string(),
591+
url: url.to_string(),
592+
code_hash: contract_info.code_hash,
593+
})
594+
}
595+
.await;
596+
597+
let cleanup_result = if processes.is_some() {
598+
terminate_nodes(&mut json_cli, processes.take(), true)
599+
.await
600+
.map_err(|e| deploy_error(e.to_string()))
601+
} else {
602+
Ok(())
603+
};
604+
605+
finalize_json_deploy_result(deploy_result, cleanup_result)
606+
}
607+
500608
async fn keep_interacting_with_node(
501609
&self,
502610
cli: &mut Cli,
@@ -629,6 +737,19 @@ impl UpContractCommand {
629737
}
630738
}
631739

740+
fn finalize_json_deploy_result<T>(
741+
deploy_result: anyhow::Result<T>,
742+
cleanup_result: anyhow::Result<()>,
743+
) -> anyhow::Result<T> {
744+
match (deploy_result, cleanup_result) {
745+
(Ok(output), Ok(())) => Ok(output),
746+
(Ok(_), Err(cleanup_error)) => Err(cleanup_error),
747+
(Err(deploy_error), Ok(())) => Err(deploy_error),
748+
// Preserve the original deploy error when both deployment and cleanup fail.
749+
(Err(deploy_error), Err(_cleanup_error)) => Err(deploy_error),
750+
}
751+
}
752+
632753
impl From<UpContractCommand> for UpOpts {
633754
fn from(cmd: UpContractCommand) -> Self {
634755
UpOpts {
@@ -905,4 +1026,49 @@ mod tests {
9051026
assert_eq!(ports.ink_node_port, DEFAULT_PORT);
9061027
assert_eq!(ports.eth_rpc_port, DEFAULT_ETH_RPC_PORT);
9071028
}
1029+
1030+
#[test]
1031+
fn finalize_json_deploy_result_returns_output_when_both_succeed() {
1032+
let output = finalize_json_deploy_result::<u32>(Ok(7), Ok(())).expect("expected success");
1033+
assert_eq!(output, 7);
1034+
}
1035+
1036+
#[test]
1037+
fn finalize_json_deploy_result_returns_cleanup_error_when_deploy_succeeds() {
1038+
let error = finalize_json_deploy_result::<u32>(Ok(7), Err(deploy_error("cleanup failed")))
1039+
.expect_err("expected cleanup failure");
1040+
assert_eq!(error.to_string(), "cleanup failed");
1041+
}
1042+
1043+
#[test]
1044+
fn finalize_json_deploy_result_returns_deploy_error_when_deploy_fails() {
1045+
let error = finalize_json_deploy_result::<u32>(Err(deploy_error("deploy failed")), Ok(()))
1046+
.expect_err("expected deploy failure");
1047+
assert_eq!(error.to_string(), "deploy failed");
1048+
}
1049+
1050+
#[test]
1051+
fn finalize_json_deploy_result_preserves_deploy_error_when_both_fail() {
1052+
let error = finalize_json_deploy_result::<u32>(
1053+
Err(deploy_error("deploy failed")),
1054+
Err(deploy_error("cleanup failed")),
1055+
)
1056+
.expect_err("expected deploy failure");
1057+
assert_eq!(error.to_string(), "deploy failed");
1058+
}
1059+
1060+
#[tokio::test]
1061+
async fn execute_json_requires_execute_before_setup_work() {
1062+
let mut cmd = UpContractCommand {
1063+
path: PathBuf::from("__non_existent_contract_path_for_execute_json_test__"),
1064+
execute: false,
1065+
..Default::default()
1066+
};
1067+
1068+
let error = cmd.execute_json().await.expect_err("expected missing execute error");
1069+
assert_eq!(
1070+
error.to_string(),
1071+
"`pop --json up` requires `--execute` to return contract deployment output",
1072+
);
1073+
}
9081074
}

0 commit comments

Comments
 (0)